Merge of compiling's multidoors branch into mine
This commit is contained in:
+2
-2
@@ -2,7 +2,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from Utils import output_path, parse_names_string
|
from Utils import output_path
|
||||||
from Rom import LocalRom, apply_rom_settings
|
from Rom import LocalRom, apply_rom_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def adjust(args):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes, parse_names_string(args.names))
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||||
|
|
||||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||||
|
|
||||||
|
|||||||
+136
-128
@@ -1,5 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique, Flag
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict, deque
|
from collections import OrderedDict, deque
|
||||||
@@ -14,6 +14,7 @@ class World(object):
|
|||||||
|
|
||||||
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
||||||
self.players = players
|
self.players = players
|
||||||
|
self.teams = 1
|
||||||
self.shuffle = shuffle.copy()
|
self.shuffle = shuffle.copy()
|
||||||
self.doorShuffle = doorShuffle.copy()
|
self.doorShuffle = doorShuffle.copy()
|
||||||
self.logic = logic.copy()
|
self.logic = logic.copy()
|
||||||
@@ -73,6 +74,8 @@ class World(object):
|
|||||||
def set_player_attr(attr, val):
|
def set_player_attr(attr, val):
|
||||||
self.__dict__.setdefault(attr, {})[player] = val
|
self.__dict__.setdefault(attr, {})[player] = val
|
||||||
set_player_attr('_region_cache', {})
|
set_player_attr('_region_cache', {})
|
||||||
|
set_player_attr('player_names', [])
|
||||||
|
set_player_attr('remote_items', False)
|
||||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||||
set_player_attr('swamp_patch_required', False)
|
set_player_attr('swamp_patch_required', False)
|
||||||
set_player_attr('powder_patch_required', False)
|
set_player_attr('powder_patch_required', False)
|
||||||
@@ -105,6 +108,12 @@ class World(object):
|
|||||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||||
set_player_attr('treasure_hunt_count', 0)
|
set_player_attr('treasure_hunt_count', 0)
|
||||||
|
|
||||||
|
def get_name_string_for_object(self, obj):
|
||||||
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||||
|
|
||||||
|
def get_player_names(self, player):
|
||||||
|
return ", ".join([name for i, name in enumerate(self.player_names[player]) if self.player_names[player].index(name) == i])
|
||||||
|
|
||||||
def initialize_regions(self, regions=None):
|
def initialize_regions(self, regions=None):
|
||||||
for region in regions if regions else self.regions:
|
for region in regions if regions else self.regions:
|
||||||
region.world = self
|
region.world = self
|
||||||
@@ -184,6 +193,19 @@ class World(object):
|
|||||||
return door
|
return door
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def check_for_entrance(self, entrance, player):
|
||||||
|
if isinstance(entrance, Entrance):
|
||||||
|
return entrance
|
||||||
|
try:
|
||||||
|
return self._entrance_cache[(entrance, player)]
|
||||||
|
except KeyError:
|
||||||
|
for region in self.regions:
|
||||||
|
for ext in region.exits:
|
||||||
|
if ext.name == entrance and ext.player == player:
|
||||||
|
self._entrance_cache[(entrance, player)] = ext
|
||||||
|
return ext
|
||||||
|
return None
|
||||||
|
|
||||||
def get_room(self, room_idx, player):
|
def get_room(self, room_idx, player):
|
||||||
if isinstance(room_idx, Room):
|
if isinstance(room_idx, Room):
|
||||||
return room_idx
|
return room_idx
|
||||||
@@ -266,6 +288,7 @@ class World(object):
|
|||||||
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
|
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
|
||||||
|
|
||||||
def push_precollected(self, item):
|
def push_precollected(self, item):
|
||||||
|
item.world = self
|
||||||
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
||||||
item.advancement = True
|
item.advancement = True
|
||||||
self.precollected_items.append(item)
|
self.precollected_items.append(item)
|
||||||
@@ -278,6 +301,7 @@ class World(object):
|
|||||||
if location.can_fill(self.state, item, False):
|
if location.can_fill(self.state, item, False):
|
||||||
location.item = item
|
location.item = item
|
||||||
item.location = location
|
item.location = location
|
||||||
|
item.world = self
|
||||||
if collect:
|
if collect:
|
||||||
self.state.collect(item, location.event, location)
|
self.state.collect(item, location.event, location)
|
||||||
|
|
||||||
@@ -349,6 +373,7 @@ class World(object):
|
|||||||
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
|
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
|
||||||
|
|
||||||
while prog_locations:
|
while prog_locations:
|
||||||
|
state.sweep_for_crystal_access()
|
||||||
sphere = []
|
sphere = []
|
||||||
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||||
for location in prog_locations:
|
for location in prog_locations:
|
||||||
@@ -368,6 +393,7 @@ class World(object):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class CollectionState(object):
|
class CollectionState(object):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
@@ -375,8 +401,7 @@ class CollectionState(object):
|
|||||||
self.world = parent
|
self.world = parent
|
||||||
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
self.reachable_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||||
self.colored_regions = {player: {} for player in range(1, parent.players + 1)}
|
self.colored_regions = {player: {} for player in range(1, parent.players + 1)}
|
||||||
# todo: I don't know if these are required to check or not, when should they be checked?
|
self.blocked_color_regions = {player: set() for player in range(1, parent.players + 1)}
|
||||||
self.crystal_connections = {player: set() for player in range(1, parent.players + 1)}
|
|
||||||
self.events = []
|
self.events = []
|
||||||
self.path = {}
|
self.path = {}
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
@@ -389,6 +414,7 @@ class CollectionState(object):
|
|||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
rrp = self.reachable_regions[player]
|
rrp = self.reachable_regions[player]
|
||||||
ccr = self.colored_regions[player]
|
ccr = self.colored_regions[player]
|
||||||
|
blocked = self.blocked_color_regions[player]
|
||||||
new_regions = True
|
new_regions = True
|
||||||
reachable_regions_count = len(rrp)
|
reachable_regions_count = len(rrp)
|
||||||
while new_regions:
|
while new_regions:
|
||||||
@@ -405,72 +431,67 @@ class CollectionState(object):
|
|||||||
c_switch_present = True
|
c_switch_present = True
|
||||||
if c_switch_present:
|
if c_switch_present:
|
||||||
ccr[candidate] = CrystalBarrier.Either
|
ccr[candidate] = CrystalBarrier.Either
|
||||||
self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, region_queue, player)
|
self.spread_crystal_access(candidate, CrystalBarrier.Either, rrp, ccr, player)
|
||||||
|
for ext in candidate.exits:
|
||||||
|
connect = ext.connected_region
|
||||||
|
if connect in rrp and not ext.can_reach(self):
|
||||||
|
blocked.add(candidate)
|
||||||
else:
|
else:
|
||||||
color_changed = False
|
color_type = CrystalBarrier.Null
|
||||||
for entrance in candidate.entrances:
|
for entrance in candidate.entrances:
|
||||||
|
if entrance.parent_region in rrp:
|
||||||
|
if entrance.can_reach(self):
|
||||||
door = self.world.check_for_door(entrance.name, player)
|
door = self.world.check_for_door(entrance.name, player)
|
||||||
if door is None or entrance.parent_region.type != RegionType.Dungeon:
|
if door is None or entrance.parent_region.type != RegionType.Dungeon:
|
||||||
ccr[candidate] = CrystalBarrier.Orange
|
color_type |= CrystalBarrier.Orange
|
||||||
else:
|
|
||||||
color_type = None
|
|
||||||
if door.crystal in [CrystalBarrier.Orange, CrystalBarrier.Blue]:
|
|
||||||
color_type = door.crystal
|
|
||||||
elif entrance.parent_region in ccr.keys():
|
elif entrance.parent_region in ccr.keys():
|
||||||
color_type = ccr[entrance.parent_region]
|
color_type |= (ccr[entrance.parent_region] & (door.crystal or CrystalBarrier.Either))
|
||||||
if color_type is not None:
|
|
||||||
current_type = ccr[candidate] if candidate in ccr.keys() else None
|
|
||||||
if current_type is None:
|
|
||||||
color_changed = True
|
|
||||||
if current_type is not None and current_type != CrystalBarrier.Either and color_type != current_type:
|
|
||||||
color_changed = True
|
|
||||||
ccr[candidate] = CrystalBarrier.Either
|
|
||||||
else:
|
else:
|
||||||
|
blocked.add(entrance.parent_region)
|
||||||
|
if color_type:
|
||||||
ccr[candidate] = color_type
|
ccr[candidate] = color_type
|
||||||
if color_changed:
|
for ext in candidate.exits:
|
||||||
self.spread_crystal_access(candidate, ccr[candidate], rrp, ccr, region_queue, player)
|
connect = ext.connected_region
|
||||||
|
if connect in rrp and connect in ccr:
|
||||||
|
door = self.world.check_for_door(ext.name, player)
|
||||||
|
if door is not None and not door.blocked:
|
||||||
|
if ext.can_reach(self):
|
||||||
|
new_color = ccr[connect] | (ccr[candidate] & (door.crystal or CrystalBarrier.Either))
|
||||||
|
if new_color != ccr[connect]:
|
||||||
|
self.spread_crystal_access(candidate, new_color, rrp, ccr, player)
|
||||||
|
else:
|
||||||
|
blocked.add(candidate)
|
||||||
new_regions = len(rrp) > reachable_regions_count
|
new_regions = len(rrp) > reachable_regions_count
|
||||||
reachable_regions_count = len(rrp)
|
reachable_regions_count = len(rrp)
|
||||||
|
|
||||||
def spread_crystal_access(self, region, crystal, rrp, ccr, region_queue, player):
|
def spread_crystal_access(self, region, crystal, rrp, ccr, player):
|
||||||
queue = deque([(region, crystal)])
|
queue = deque([(region, crystal)])
|
||||||
visited = set()
|
visited = set()
|
||||||
|
updated = False
|
||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
region, crystal = queue.popleft()
|
region, crystal = queue.popleft()
|
||||||
visited.add(region)
|
visited.add(region)
|
||||||
for ext in region.exits:
|
for ext in region.exits:
|
||||||
connect = ext.connected_region
|
connect = ext.connected_region
|
||||||
if connect not in visited and connect is not None and connect.type == RegionType.Dungeon:
|
if connect is not None and connect.type == RegionType.Dungeon:
|
||||||
if connect in rrp:
|
if connect not in visited and connect in rrp and connect in ccr:
|
||||||
if ext.can_reach(self):
|
if ext.can_reach(self):
|
||||||
rc_pair = self.spread_crystal_door(crystal, ext, connect, ccr, player)
|
|
||||||
if rc_pair is not None:
|
|
||||||
queue.append(rc_pair)
|
|
||||||
else:
|
|
||||||
self.crystal_connections[player].add((ext, crystal))
|
|
||||||
|
|
||||||
elif connect not in rrp and connect not in region_queue and connect.can_reach_private(self):
|
|
||||||
region_queue.append(connect)
|
|
||||||
|
|
||||||
def spread_crystal_door(self, crystal, ext, connect, ccr, player):
|
|
||||||
door = self.world.check_for_door(ext.name, player)
|
door = self.world.check_for_door(ext.name, player)
|
||||||
|
if door is not None and not door.blocked:
|
||||||
current_crystal = ccr[connect]
|
current_crystal = ccr[connect]
|
||||||
if door is not None and not door.blocked and current_crystal != crystal and current_crystal != CrystalBarrier.Either:
|
new_crystal = current_crystal | (crystal & (door.crystal or CrystalBarrier.Either))
|
||||||
if door.crystal in [CrystalBarrier.Either, CrystalBarrier.Null]:
|
if current_crystal != new_crystal:
|
||||||
ccr[connect] = crystal
|
updated = True
|
||||||
return connect, crystal
|
ccr[connect] = new_crystal
|
||||||
else:
|
queue.append((connect, new_crystal))
|
||||||
if door.crystal != current_crystal:
|
return updated
|
||||||
ccr[connect] = CrystalBarrier.Either
|
|
||||||
return connect, door.crystal
|
|
||||||
return None
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
ret = CollectionState(self.world)
|
ret = CollectionState(self.world)
|
||||||
ret.prog_items = self.prog_items.copy()
|
ret.prog_items = self.prog_items.copy()
|
||||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.colored_regions = {player: copy.copy(self.colored_regions[player]) for player in range(1, self.world.players + 1)}
|
ret.colored_regions = {player: copy.copy(self.colored_regions[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.crystal_connections = {player: copy.copy(self.crystal_connections[player]) for player in range(1, self.world.players + 1)}
|
ret.blocked_color_regions = {player: copy.copy(self.blocked_color_regions[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.events = copy.copy(self.events)
|
ret.events = copy.copy(self.events)
|
||||||
ret.path = copy.copy(self.path)
|
ret.path = copy.copy(self.path)
|
||||||
ret.locations_checked = copy.copy(self.locations_checked)
|
ret.locations_checked = copy.copy(self.locations_checked)
|
||||||
@@ -491,27 +512,19 @@ class CollectionState(object):
|
|||||||
|
|
||||||
return spot.can_reach(self)
|
return spot.can_reach(self)
|
||||||
|
|
||||||
# def sweep_for_crystal_access(self):
|
def sweep_for_crystal_access(self):
|
||||||
# repeat = True
|
for player, rrp in self.reachable_regions.items():
|
||||||
# while repeat:
|
updated = True
|
||||||
# repeat = False
|
while updated:
|
||||||
# for player, crystal_conn in self.crystal_connections.items():
|
if self.stale[player]:
|
||||||
# self.update_reachable_regions(player)
|
self.update_reachable_regions(player)
|
||||||
# new_regions = deque()
|
updated = False
|
||||||
# remove_list = []
|
dungeon_regions = self.blocked_color_regions[player]
|
||||||
# for connection, crystal in crystal_conn:
|
ccr = self.colored_regions[player]
|
||||||
# if connection.can_reach(self):
|
for region in dungeon_regions.copy():
|
||||||
# remove_list.append((connection, crystal))
|
if region in ccr.keys():
|
||||||
# rrp, ccr = self.reachable_regions[player], self.colored_regions[player]
|
updated |= self.spread_crystal_access(region, ccr[region], rrp, ccr, player)
|
||||||
# rc_pair = self.spread_crystal_door(crystal, connection, connection.parent_region, ccr, player)
|
self.stale[player] = updated
|
||||||
# if rc_pair is not None:
|
|
||||||
# conn, new_crystal = rc_pair
|
|
||||||
# self.spread_crystal_access(conn, new_crystal, rrp, ccr, new_regions, player)
|
|
||||||
# for pair in remove_list:
|
|
||||||
# crystal_conn.remove(pair)
|
|
||||||
# if len(new_regions) > 0:
|
|
||||||
# self.update_reachable_regions(player)
|
|
||||||
# repeat = True
|
|
||||||
|
|
||||||
def sweep_for_events(self, key_only=False, locations=None):
|
def sweep_for_events(self, key_only=False, locations=None):
|
||||||
# this may need improvement
|
# this may need improvement
|
||||||
@@ -530,8 +543,6 @@ class CollectionState(object):
|
|||||||
self.collect(event.item, True, event)
|
self.collect(event.item, True, event)
|
||||||
new_locations = len(reachable_events) > checked_locations
|
new_locations = len(reachable_events) > checked_locations
|
||||||
checked_locations = len(reachable_events)
|
checked_locations = len(reachable_events)
|
||||||
# if new_locations:
|
|
||||||
# self.sweep_for_crystal_access()
|
|
||||||
|
|
||||||
def can_reach_blue(self, region, player):
|
def can_reach_blue(self, region, player):
|
||||||
if region not in self.colored_regions[player].keys():
|
if region not in self.colored_regions[player].keys():
|
||||||
@@ -884,10 +895,7 @@ class Region(object):
|
|||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.world and self.world.players == 1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Entrance(object):
|
class Entrance(object):
|
||||||
@@ -903,6 +911,7 @@ class Entrance(object):
|
|||||||
self.vanilla = None
|
self.vanilla = None
|
||||||
self.access_rule = lambda state: True
|
self.access_rule = lambda state: True
|
||||||
self.player = player
|
self.player = player
|
||||||
|
self.door = None
|
||||||
|
|
||||||
def can_reach(self, state):
|
def can_reach(self, state):
|
||||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
if self.parent_region.can_reach(state) and self.access_rule(state):
|
||||||
@@ -923,11 +932,8 @@ class Entrance(object):
|
|||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
|
world = self.parent_region.world if self.parent_region else None
|
||||||
return self.name
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Dungeon(object):
|
class Dungeon(object):
|
||||||
|
|
||||||
@@ -976,10 +982,7 @@ class Dungeon(object):
|
|||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.world and self.world.players==1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
@@ -1080,15 +1083,15 @@ pol_comp = {
|
|||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class CrystalBarrier(Enum):
|
class CrystalBarrier(Flag):
|
||||||
Null = 1 # no special requirement
|
Null = 0 # no special requirement
|
||||||
Blue = 2 # blue must be down and explore state set to Blue
|
Blue = 1 # blue must be down and explore state set to Blue
|
||||||
Orange = 3 # orange must be down and explore state set to Orange
|
Orange = 2 # orange must be down and explore state set to Orange
|
||||||
Either = 4 # you choose to leave this room in Either state
|
Either = 3 # you choose to leave this room in Either state
|
||||||
|
|
||||||
|
|
||||||
class Door(object):
|
class Door(object):
|
||||||
def __init__(self, player, name, type):
|
def __init__(self, player, name, type, entrance=None):
|
||||||
self.player = player
|
self.player = player
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type
|
self.type = type
|
||||||
@@ -1123,6 +1126,10 @@ class Door(object):
|
|||||||
self.dependents = []
|
self.dependents = []
|
||||||
self.dead = False
|
self.dead = False
|
||||||
|
|
||||||
|
self.entrance = entrance
|
||||||
|
if entrance is not None:
|
||||||
|
entrance.door = self
|
||||||
|
|
||||||
def getAddress(self):
|
def getAddress(self):
|
||||||
if self.type == DoorType.Normal:
|
if self.type == DoorType.Normal:
|
||||||
return 0x13A000 + normal_offset_table[self.roomIndex] * 24 + (self.doorIndex + self.direction.value * 3) * 2
|
return 0x13A000 + normal_offset_table[self.roomIndex] * 24 + (self.doorIndex + self.direction.value * 3) * 2
|
||||||
@@ -1317,10 +1324,8 @@ class Location(object):
|
|||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.parent_region and self.parent_region.world and self.parent_region.world.players == 1:
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
||||||
return self.name
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
class Item(object):
|
class Item(object):
|
||||||
@@ -1339,6 +1344,7 @@ class Item(object):
|
|||||||
self.hint_text = hint_text
|
self.hint_text = hint_text
|
||||||
self.code = code
|
self.code = code
|
||||||
self.location = None
|
self.location = None
|
||||||
|
self.world = None
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1365,10 +1371,7 @@ class Item(object):
|
|||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.location and self.location.parent_region and self.location.parent_region.world and self.location.parent_region.world.players == 1:
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||||
return self.name
|
|
||||||
else:
|
|
||||||
return '%s (Player %d)' % (self.name, self.player)
|
|
||||||
|
|
||||||
|
|
||||||
# have 6 address that need to be filled
|
# have 6 address that need to be filled
|
||||||
@@ -1441,6 +1444,7 @@ class Spoiler(object):
|
|||||||
|
|
||||||
def __init__(self, world):
|
def __init__(self, world):
|
||||||
self.world = world
|
self.world = world
|
||||||
|
self.hashes = {}
|
||||||
self.entrances = OrderedDict()
|
self.entrances = OrderedDict()
|
||||||
self.doors = OrderedDict()
|
self.doors = OrderedDict()
|
||||||
self.doorTypes = OrderedDict()
|
self.doorTypes = OrderedDict()
|
||||||
@@ -1479,8 +1483,8 @@ class Spoiler(object):
|
|||||||
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
||||||
else:
|
else:
|
||||||
for player in range(1, self.world.players + 1):
|
for player in range(1, self.world.players + 1):
|
||||||
self.medallions['Misery Mire (Player %d)' % player] = self.world.required_medallions[player][0]
|
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
||||||
self.medallions['Turtle Rock (Player %d)' % player] = self.world.required_medallions[player][1]
|
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
||||||
|
|
||||||
self.startinventory = list(map(str, self.world.precollected_items))
|
self.startinventory = list(map(str, self.world.precollected_items))
|
||||||
|
|
||||||
@@ -1569,7 +1573,8 @@ class Spoiler(object):
|
|||||||
'enemy_shuffle': self.world.enemy_shuffle,
|
'enemy_shuffle': self.world.enemy_shuffle,
|
||||||
'enemy_health': self.world.enemy_health,
|
'enemy_health': self.world.enemy_health,
|
||||||
'enemy_damage': self.world.enemy_damage,
|
'enemy_damage': self.world.enemy_damage,
|
||||||
'players': self.world.players
|
'players': self.world.players,
|
||||||
|
'teams': self.world.teams
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
@@ -1581,6 +1586,8 @@ class Spoiler(object):
|
|||||||
out.update(self.locations)
|
out.update(self.locations)
|
||||||
out['Starting Inventory'] = self.startinventory
|
out['Starting Inventory'] = self.startinventory
|
||||||
out['Special'] = self.medallions
|
out['Special'] = self.medallions
|
||||||
|
if self.hashes:
|
||||||
|
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
||||||
if self.shops:
|
if self.shops:
|
||||||
out['Shops'] = self.shops
|
out['Shops'] = self.shops
|
||||||
out['playthrough'] = self.playthrough
|
out['playthrough'] = self.playthrough
|
||||||
@@ -1594,30 +1601,35 @@ class Spoiler(object):
|
|||||||
self.parse_data()
|
self.parse_data()
|
||||||
with open(filename, 'w') as outfile:
|
with open(filename, 'w') as outfile:
|
||||||
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
|
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
|
||||||
outfile.write('Players: %d\n' % self.world.players)
|
|
||||||
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
|
||||||
outfile.write('Logic: %s\n' % self.metadata['logic'])
|
outfile.write('Players: %d\n' % self.world.players)
|
||||||
outfile.write('Mode: %s\n' % self.metadata['mode'])
|
outfile.write('Teams: %d\n' % self.world.teams)
|
||||||
outfile.write('Retro: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['retro'].items()})
|
for player in range(1, self.world.players + 1):
|
||||||
outfile.write('Swords: %s\n' % self.metadata['weapons'])
|
if self.world.players > 1:
|
||||||
outfile.write('Goal: %s\n' % self.metadata['goal'])
|
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
|
||||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'])
|
for team in range(self.world.teams):
|
||||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'])
|
outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team]))
|
||||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'])
|
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
|
||||||
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'])
|
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||||
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'])
|
outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
|
||||||
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'])
|
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
||||||
outfile.write('Pyramid hole pre-opened: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['open_pyramid'].items()})
|
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'])
|
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||||
outfile.write('Map shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['mapshuffle'].items()})
|
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||||
outfile.write('Compass shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['compassshuffle'].items()})
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||||
outfile.write('Small Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['keyshuffle'].items()})
|
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
|
||||||
outfile.write('Big Key shuffle: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['bigkeyshuffle'].items()})
|
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
|
||||||
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'])
|
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||||
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'])
|
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'])
|
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'])
|
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||||
outfile.write('Hints: %s\n' % {k: 'Yes' if v else 'No' for k, v in self.metadata['hints'].items()})
|
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
|
||||||
|
outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
||||||
|
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
|
||||||
|
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
|
||||||
|
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||||
|
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||||
|
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
||||||
if self.doors:
|
if self.doors:
|
||||||
outfile.write('\n\nDoors:\n\n')
|
outfile.write('\n\nDoors:\n\n')
|
||||||
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()]))
|
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()]))
|
||||||
@@ -1626,15 +1638,11 @@ class Spoiler(object):
|
|||||||
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()]))
|
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()]))
|
||||||
if self.entrances:
|
if self.entrances:
|
||||||
outfile.write('\n\nEntrances:\n\n')
|
outfile.write('\n\nEntrances:\n\n')
|
||||||
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()]))
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()]))
|
||||||
outfile.write('\n\nMedallions\n')
|
outfile.write('\n\nMedallions:\n')
|
||||||
if self.world.players == 1:
|
for dungeon, medallion in self.medallions.items():
|
||||||
outfile.write('\nMisery Mire Medallion: %s' % (self.medallions['Misery Mire']))
|
outfile.write(f'\n{dungeon}: {medallion}')
|
||||||
outfile.write('\nTurtle Rock Medallion: %s' % (self.medallions['Turtle Rock']))
|
if self.startinventory:
|
||||||
else:
|
|
||||||
for player in range(1, self.world.players + 1):
|
|
||||||
outfile.write('\nMisery Mire Medallion (Player %d): %s' % (player, self.medallions['Misery Mire (Player %d)' % player]))
|
|
||||||
outfile.write('\nTurtle Rock Medallion (Player %d): %s' % (player, self.medallions['Turtle Rock (Player %d)' % player]))
|
|
||||||
outfile.write('\n\nStarting Inventory:\n\n')
|
outfile.write('\n\nStarting Inventory:\n\n')
|
||||||
outfile.write('\n'.join(self.startinventory))
|
outfile.write('\n'.join(self.startinventory))
|
||||||
outfile.write('\n\nLocations:\n\n')
|
outfile.write('\n\nLocations:\n\n')
|
||||||
|
|||||||
+35
-22
@@ -13,7 +13,7 @@ from Dungeons import dungeon_regions, region_starts, split_region_starts, flexib
|
|||||||
from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys
|
from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys
|
||||||
from Items import ItemFactory
|
from Items import ItemFactory
|
||||||
from RoomData import DoorKind, PairedDoor
|
from RoomData import DoorKind, PairedDoor
|
||||||
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon
|
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, validate_tr
|
||||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder
|
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder
|
||||||
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
|
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
|
||||||
|
|
||||||
@@ -118,8 +118,24 @@ def vanilla_key_logic(world, player):
|
|||||||
|
|
||||||
overworld_prep(world, player)
|
overworld_prep(world, player)
|
||||||
entrances_map, potentials, connections = determine_entrance_list(world, player)
|
entrances_map, potentials, connections = determine_entrance_list(world, player)
|
||||||
for builder in builders:
|
|
||||||
start_regions = convert_regions(entrances_map[builder.name], world, player)
|
enabled_entrances = {}
|
||||||
|
sector_queue = collections.deque(builders)
|
||||||
|
last_key = None
|
||||||
|
while len(sector_queue) > 0:
|
||||||
|
builder = sector_queue.popleft()
|
||||||
|
|
||||||
|
origin_list = list(entrances_map[builder.name])
|
||||||
|
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
|
||||||
|
origin_list_sans_drops = remove_drop_origins(origin_list)
|
||||||
|
if len(origin_list_sans_drops) <= 0:
|
||||||
|
if last_key == builder.name:
|
||||||
|
raise Exception('Infinte loop detected %s' % builder.name)
|
||||||
|
sector_queue.append(builder)
|
||||||
|
last_key = builder.name
|
||||||
|
else:
|
||||||
|
find_new_entrances(builder.master_sector, connections, potentials, enabled_entrances, world, player)
|
||||||
|
start_regions = convert_regions(origin_list, world, player)
|
||||||
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
||||||
key_layout = build_key_layout(builder, start_regions, doors, world, player)
|
key_layout = build_key_layout(builder, start_regions, doors, world, player)
|
||||||
valid = validate_key_layout(key_layout, world, player)
|
valid = validate_key_layout(key_layout, world, player)
|
||||||
@@ -129,6 +145,7 @@ def vanilla_key_logic(world, player):
|
|||||||
world.key_logic[player] = {}
|
world.key_logic[player] = {}
|
||||||
analyze_dungeon(key_layout, world, player)
|
analyze_dungeon(key_layout, world, player)
|
||||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||||
|
last_key = None
|
||||||
validate_vanilla_key_logic(world, player)
|
validate_vanilla_key_logic(world, player)
|
||||||
|
|
||||||
|
|
||||||
@@ -322,7 +339,7 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
|
|||||||
origin_list = list(builder.entrance_list)
|
origin_list = list(builder.entrance_list)
|
||||||
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
|
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
|
||||||
origin_list_sans_drops = remove_drop_origins(origin_list)
|
origin_list_sans_drops = remove_drop_origins(origin_list)
|
||||||
if len(origin_list_sans_drops) <= 0:
|
if len(origin_list_sans_drops) <= 0 or name == "Turtle Rock" and not validate_tr(name, builder.sectors, origin_list_sans_drops, world, player):
|
||||||
if last_key == builder.name:
|
if last_key == builder.name:
|
||||||
raise Exception('Infinte loop detected %s' % builder.name)
|
raise Exception('Infinte loop detected %s' % builder.name)
|
||||||
sector_queue.append(builder)
|
sector_queue.append(builder)
|
||||||
@@ -1108,26 +1125,21 @@ def change_door_to_small_key(d, world, player):
|
|||||||
|
|
||||||
def determine_required_paths(world, player):
|
def determine_required_paths(world, player):
|
||||||
paths = {
|
paths = {
|
||||||
'Hyrule Castle': [],
|
'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby'],
|
||||||
'Eastern Palace': ['Eastern Boss'],
|
'Eastern Palace': ['Eastern Boss'],
|
||||||
'Desert Palace': ['Desert Boss'],
|
'Desert Palace': ['Desert Main Lobby', 'Desert East Lobby', 'Desert West Lobby', 'Desert Boss'],
|
||||||
'Tower of Hera': ['Hera Boss'],
|
'Tower of Hera': ['Hera Boss'],
|
||||||
'Agahnims Tower': ['Tower Agahnim 1'],
|
'Agahnims Tower': ['Tower Agahnim 1'],
|
||||||
'Palace of Darkness': ['PoD Boss'],
|
'Palace of Darkness': ['PoD Boss'],
|
||||||
'Swamp Palace': ['Swamp Boss'],
|
'Swamp Palace': ['Swamp Boss'],
|
||||||
'Skull Woods': ['Skull Boss'],
|
'Skull Woods': ['Skull 1 Lobby', 'Skull 2 East Lobby', 'Skull 2 West Lobby', 'Skull Boss'],
|
||||||
'Thieves Town': ['Thieves Boss', ('Thieves Blind\'s Cell', 'Thieves Boss')],
|
'Thieves Town': ['Thieves Boss', ('Thieves Blind\'s Cell', 'Thieves Boss')],
|
||||||
'Ice Palace': ['Ice Boss'],
|
'Ice Palace': ['Ice Boss'],
|
||||||
'Misery Mire': ['Mire Boss'],
|
'Misery Mire': ['Mire Boss'],
|
||||||
'Turtle Rock': ['TR Boss'],
|
'Turtle Rock': ['TR Main Lobby', 'TR Lazy Eyes', 'TR Big Chest Entrance', 'TR Eye Bridge', 'TR Boss'],
|
||||||
'Ganons Tower': ['GT Agahnim 2']
|
'Ganons Tower': ['GT Agahnim 2']
|
||||||
}
|
}
|
||||||
if world.shuffle[player] == 'vanilla':
|
if world.mode[player] == 'standard':
|
||||||
paths['Skull Woods'].insert(0, 'Skull 2 West Lobby')
|
|
||||||
paths['Turtle Rock'].insert(0, 'TR Eye Bridge')
|
|
||||||
paths['Turtle Rock'].insert(0, 'TR Big Chest Entrance')
|
|
||||||
paths['Turtle Rock'].insert(0, 'TR Lazy Eyes')
|
|
||||||
if world.mode == 'standard':
|
|
||||||
paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock')
|
paths['Hyrule Castle'].append('Hyrule Dungeon Cellblock')
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
paths['Hyrule Castle'].append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
||||||
@@ -1159,7 +1171,7 @@ def find_inaccessible_regions(world, player):
|
|||||||
if connect is not None and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions:
|
if connect is not None and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions:
|
||||||
queue.append(connect)
|
queue.append(connect)
|
||||||
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
|
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
|
||||||
if world.mode == 'standard':
|
if world.mode[player] == 'standard':
|
||||||
world.inaccessible_regions[player].append('Hyrule Castle Ledge')
|
world.inaccessible_regions[player].append('Hyrule Castle Ledge')
|
||||||
world.inaccessible_regions[player].append('Sewer Drop')
|
world.inaccessible_regions[player].append('Sewer Drop')
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
@@ -1194,19 +1206,20 @@ def add_inaccessible_doors(world, player):
|
|||||||
create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge')
|
create_door(world, player, 'Death Mountain Return Cave (West)', 'Death Mountain Return Ledge')
|
||||||
if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]:
|
if 'Desert Palace Lone Stairs' in world.inaccessible_regions[player]:
|
||||||
create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs')
|
create_door(world, player, 'Desert Palace Entrance (East)', 'Desert Palace Lone Stairs')
|
||||||
if world.mode[player] == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
|
# if world.mode[player] == 'standard' and 'Hyrule Castle Ledge' in world.inaccessible_regions[player]:
|
||||||
create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge')
|
# create_door(world, player, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Ledge')
|
||||||
create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge')
|
# create_door(world, player, 'Hyrule Castle Entrance (West)', 'Hyrule Castle Ledge')
|
||||||
|
|
||||||
|
|
||||||
def create_door(world, player, entName, region_name):
|
def create_door(world, player, entName, region_name):
|
||||||
connect = world.get_entrance(entName, player).connected_region
|
entrance = world.get_entrance(entName, player)
|
||||||
|
connect = entrance.connected_region
|
||||||
for ext in connect.exits:
|
for ext in connect.exits:
|
||||||
if ext.connected_region is not None and ext.connected_region.name == region_name:
|
if ext.connected_region is not None and ext.connected_region.name == region_name:
|
||||||
d = Door(player, ext.name, DoorType.Logical),
|
d = Door(player, ext.name, DoorType.Logical, ext),
|
||||||
world.doors += d
|
world.doors += d
|
||||||
connect_door_only(world, ext.name, ext.connected_region, player)
|
connect_door_only(world, ext.name, ext.connected_region, player)
|
||||||
d = Door(player, entName, DoorType.Logical),
|
d = Door(player, entName, DoorType.Logical, entrance),
|
||||||
world.doors += d
|
world.doors += d
|
||||||
connect_door_only(world, entName, connect, player)
|
connect_door_only(world, entName, connect, player)
|
||||||
|
|
||||||
@@ -1286,7 +1299,7 @@ def check_if_regions_visited(state, check_paths):
|
|||||||
|
|
||||||
def check_for_pinball_fix(state, bad_region, world, player):
|
def check_for_pinball_fix(state, bad_region, world, player):
|
||||||
pinball_region = world.get_region('Skull Pinball', player)
|
pinball_region = world.get_region('Skull Pinball', player)
|
||||||
if bad_region.name == 'Skull 2 West Lobby' and state.visited_at_all(pinball_region):
|
if bad_region.name == 'Skull 2 West Lobby' and state.visited_at_all(pinball_region): #revisit this for entrance shuffle
|
||||||
door = world.get_door('Skull Pinball WS', player)
|
door = world.get_door('Skull Pinball WS', player)
|
||||||
room = world.get_room(door.roomIndex, player)
|
room = world.get_room(door.roomIndex, player)
|
||||||
if room.doorList[door.doorListPos][1] == DoorKind.Trap:
|
if room.doorList[door.doorListPos][1] == DoorKind.Trap:
|
||||||
|
|||||||
@@ -1069,6 +1069,9 @@ def create_doors(world, player):
|
|||||||
world.get_door('Swamp Drain Right Switch', player).event('Swamp Drain')
|
world.get_door('Swamp Drain Right Switch', player).event('Swamp Drain')
|
||||||
world.get_door('Swamp Flooded Room Ladder', player).event('Swamp Drain')
|
world.get_door('Swamp Flooded Room Ladder', player).event('Swamp Drain')
|
||||||
|
|
||||||
|
if world.mode[player] == 'standard':
|
||||||
|
world.get_door('Hyrule Castle Throne Room N', player).event('Zelda Pickup')
|
||||||
|
|
||||||
# crystal switches and barriers
|
# crystal switches and barriers
|
||||||
world.get_door('Hera Lobby Down Stairs', player).c_switch()
|
world.get_door('Hera Lobby Down Stairs', player).c_switch()
|
||||||
world.get_door('Hera Lobby Key Stairs', player).c_switch()
|
world.get_door('Hera Lobby Key Stairs', player).c_switch()
|
||||||
@@ -1203,6 +1206,8 @@ def create_doors(world, player):
|
|||||||
controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player))
|
controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player))
|
||||||
controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player))
|
controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player))
|
||||||
|
|
||||||
|
assign_entrances(world, player)
|
||||||
|
|
||||||
|
|
||||||
def create_paired_doors(world, player):
|
def create_paired_doors(world, player):
|
||||||
world.paired_doors[player] = [
|
world.paired_doors[player] = [
|
||||||
@@ -1247,6 +1252,15 @@ def create_paired_doors(world, player):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def assign_entrances(world, player):
|
||||||
|
for door in world.doors:
|
||||||
|
if door.player == player:
|
||||||
|
entrance = world.check_for_entrance(door.name, player)
|
||||||
|
if entrance is not None:
|
||||||
|
door.entrance = entrance
|
||||||
|
entrance.door = door
|
||||||
|
|
||||||
|
|
||||||
def controller_door(controller, dependent):
|
def controller_door(controller, dependent):
|
||||||
dependent.controller = controller
|
dependent.controller = controller
|
||||||
controller.dependents.append(dependent)
|
controller.dependents.append(dependent)
|
||||||
|
|||||||
+85
-36
@@ -1,6 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
import collections
|
import collections
|
||||||
from collections import defaultdict
|
from collections import defaultdict, deque
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
import logging
|
import logging
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
@@ -31,16 +31,35 @@ class GraphPiece:
|
|||||||
self.possible_bk_locations = set()
|
self.possible_bk_locations = set()
|
||||||
|
|
||||||
|
|
||||||
def generate_dungeon(name, available_sectors, entrance_region_names, split_dungeon, world, player):
|
# Turtle Rock shouldn't be generated until the Big Chest entrance is reachable
|
||||||
logger = logging.getLogger('')
|
def validate_tr(name, available_sectors, entrance_region_names, world, player):
|
||||||
entrance_regions = convert_regions(entrance_region_names, world, player)
|
entrance_regions = convert_regions(entrance_region_names, world, player)
|
||||||
doors_to_connect = set()
|
proposed_map = {}
|
||||||
|
doors_to_connect = {}
|
||||||
all_regions = set()
|
all_regions = set()
|
||||||
bk_needed = False
|
bk_needed = False
|
||||||
bk_special = False
|
bk_special = False
|
||||||
for sector in available_sectors:
|
for sector in available_sectors:
|
||||||
for door in sector.outstanding_doors:
|
for door in sector.outstanding_doors:
|
||||||
doors_to_connect.add(door)
|
doors_to_connect[door.name] = door
|
||||||
|
all_regions.update(sector.regions)
|
||||||
|
bk_needed = bk_needed or determine_if_bk_needed(sector, False, world, player)
|
||||||
|
bk_special = bk_special or check_for_special(sector)
|
||||||
|
dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map,
|
||||||
|
doors_to_connect, bk_needed, bk_special, world, player)
|
||||||
|
return check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, False)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_dungeon(name, available_sectors, entrance_region_names, split_dungeon, world, player):
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
entrance_regions = convert_regions(entrance_region_names, world, player)
|
||||||
|
doors_to_connect = {}
|
||||||
|
all_regions = set()
|
||||||
|
bk_needed = False
|
||||||
|
bk_special = False
|
||||||
|
for sector in available_sectors:
|
||||||
|
for door in sector.outstanding_doors:
|
||||||
|
doors_to_connect[door.name] = door
|
||||||
all_regions.update(sector.regions)
|
all_regions.update(sector.regions)
|
||||||
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
||||||
bk_special = bk_special or check_for_special(sector)
|
bk_special = bk_special or check_for_special(sector)
|
||||||
@@ -51,7 +70,8 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
|
|||||||
backtrack = False
|
backtrack = False
|
||||||
itr = 0
|
itr = 0
|
||||||
finished = False
|
finished = False
|
||||||
# last_choice = None
|
# flag if standard and this is hyrule castle
|
||||||
|
std_flag = world.mode[player] == 'standard' and bk_special
|
||||||
while not finished:
|
while not finished:
|
||||||
# what are my choices?
|
# what are my choices?
|
||||||
itr += 1
|
itr += 1
|
||||||
@@ -61,7 +81,7 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
|
|||||||
dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map,
|
dungeon, hangers, hooks = gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map,
|
||||||
doors_to_connect, bk_needed, bk_special, world, player)
|
doors_to_connect, bk_needed, bk_special, world, player)
|
||||||
dungeon_cache[depth] = dungeon, hangers, hooks
|
dungeon_cache[depth] = dungeon, hangers, hooks
|
||||||
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed)
|
valid = check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, std_flag)
|
||||||
else:
|
else:
|
||||||
dungeon, hangers, hooks = dungeon_cache[depth]
|
dungeon, hangers, hooks = dungeon_cache[depth]
|
||||||
valid = True
|
valid = True
|
||||||
@@ -98,7 +118,7 @@ def generate_dungeon(name, available_sectors, entrance_region_names, split_dunge
|
|||||||
queue = collections.deque(proposed_map.items())
|
queue = collections.deque(proposed_map.items())
|
||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
a, b = queue.pop()
|
a, b = queue.pop()
|
||||||
connect_doors(a, b, world, player)
|
connect_doors(a, b)
|
||||||
queue.remove((b, a))
|
queue.remove((b, a))
|
||||||
master_sector = available_sectors.pop()
|
master_sector = available_sectors.pop()
|
||||||
for sub_sector in available_sectors:
|
for sub_sector in available_sectors:
|
||||||
@@ -136,7 +156,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, proposed_map, va
|
|||||||
for door in sector.outstanding_doors:
|
for door in sector.outstanding_doors:
|
||||||
if not door.stonewall and door not in proposed_map.keys():
|
if not door.stonewall and door not in proposed_map.keys():
|
||||||
hanger_set.add(door)
|
hanger_set.add(door)
|
||||||
parent = parent_region(door, world, player).parent_region
|
parent = door.entrance.parent_region
|
||||||
init_state = ExplorationState(dungeon=name)
|
init_state = ExplorationState(dungeon=name)
|
||||||
init_state.big_key_special = start.big_key_special
|
init_state.big_key_special = start.big_key_special
|
||||||
o_state = extend_reachable_state_improved([parent], init_state, proposed_map,
|
o_state = extend_reachable_state_improved([parent], init_state, proposed_map,
|
||||||
@@ -198,7 +218,7 @@ def check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, valid_do
|
|||||||
|
|
||||||
|
|
||||||
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
|
def explore_blue_state(door, dungeon, o_state, proposed_map, valid_doors, world, player):
|
||||||
parent = parent_region(door, world, player).parent_region
|
parent = door.entrance.parent_region
|
||||||
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon)
|
blue_start = ExplorationState(CrystalBarrier.Blue, o_state.dungeon)
|
||||||
blue_start.big_key_special = o_state.big_key_special
|
blue_start.big_key_special = o_state.big_key_special
|
||||||
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player)
|
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, valid_doors, False, world, player)
|
||||||
@@ -269,7 +289,7 @@ def filter_choices(next_hanger, door, orig_hang, prev_choices, hook_candidates):
|
|||||||
return next_hanger != door and orig_hang != next_hanger and door not in hook_candidates
|
return next_hanger != door and orig_hang != next_hanger and door not in hook_candidates
|
||||||
|
|
||||||
|
|
||||||
def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed):
|
def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions, bk_needed, std_flag):
|
||||||
# evaluate if everything is still plausible
|
# evaluate if everything is still plausible
|
||||||
|
|
||||||
# only origin is left in the dungeon and not everything is connected
|
# only origin is left in the dungeon and not everything is connected
|
||||||
@@ -300,7 +320,7 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg
|
|||||||
if len(must_hang[key]) > len(hooks[key]):
|
if len(must_hang[key]) > len(hooks[key]):
|
||||||
return False
|
return False
|
||||||
outstanding_doors = defaultdict(list)
|
outstanding_doors = defaultdict(list)
|
||||||
for d in doors_to_connect:
|
for d in doors_to_connect.values():
|
||||||
if d not in proposed_map.keys():
|
if d not in proposed_map.keys():
|
||||||
outstanding_doors[hook_from_door(d)].append(d)
|
outstanding_doors[hook_from_door(d)].append(d)
|
||||||
for key in outstanding_doors.keys():
|
for key in outstanding_doors.keys():
|
||||||
@@ -317,6 +337,8 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg
|
|||||||
return False
|
return False
|
||||||
if not bk_possible:
|
if not bk_possible:
|
||||||
return False
|
return False
|
||||||
|
if std_flag and not cellblock_valid(doors_to_connect, all_regions, proposed_map):
|
||||||
|
return False
|
||||||
new_hangers_found = True
|
new_hangers_found = True
|
||||||
accessible_hook_types = []
|
accessible_hook_types = []
|
||||||
hanger_matching = set()
|
hanger_matching = set()
|
||||||
@@ -344,6 +366,41 @@ def check_valid(dungeon, hangers, hooks, proposed_map, doors_to_connect, all_reg
|
|||||||
return len(all_hangers.difference(hanger_matching)) == 0
|
return len(all_hangers.difference(hanger_matching)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def cellblock_valid(valid_doors, all_regions, proposed_map):
|
||||||
|
cellblock = None
|
||||||
|
for region in all_regions:
|
||||||
|
if 'Hyrule Dungeon Cellblock' == region.name:
|
||||||
|
cellblock = region
|
||||||
|
break
|
||||||
|
queue = deque([cellblock])
|
||||||
|
visited = {cellblock}
|
||||||
|
while len(queue) > 0:
|
||||||
|
region = queue.popleft()
|
||||||
|
if region.name == 'Sanctuary':
|
||||||
|
return True
|
||||||
|
for ext in region.exits:
|
||||||
|
connect = ext.connected_region
|
||||||
|
if connect is None and ext.name in valid_doors:
|
||||||
|
door = valid_doors[ext.name]
|
||||||
|
if not door.blocked:
|
||||||
|
if door in proposed_map:
|
||||||
|
new_region = proposed_map[door].entrance.parent_region
|
||||||
|
if new_region not in visited:
|
||||||
|
visited.add(new_region)
|
||||||
|
queue.append(new_region)
|
||||||
|
else:
|
||||||
|
return True # outstanding connection possible
|
||||||
|
elif connect is not None:
|
||||||
|
door = ext.door
|
||||||
|
if door is not None and not door.blocked and connect not in visited:
|
||||||
|
visited.add(connect)
|
||||||
|
queue.append(connect)
|
||||||
|
return False # couldn't find an outstanding door or the sanctuary
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def winnow_hangers(hangers, hooks):
|
def winnow_hangers(hangers, hooks):
|
||||||
removal_info = []
|
removal_info = []
|
||||||
for hanger, door_set in hangers.items():
|
for hanger, door_set in hangers.items():
|
||||||
@@ -406,10 +463,6 @@ def filter_for_potential_bk_locations(locations):
|
|||||||
and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']]
|
and x.name not in key_only_locations.keys() and x.name not in ['Agahnim 1', 'Agahnim 2']]
|
||||||
|
|
||||||
|
|
||||||
def parent_region(door, world, player):
|
|
||||||
return world.get_entrance(door.name, player)
|
|
||||||
|
|
||||||
|
|
||||||
def opposite_h_type(h_type):
|
def opposite_h_type(h_type):
|
||||||
type_map = {
|
type_map = {
|
||||||
Hook.Stairs: Hook.Stairs,
|
Hook.Stairs: Hook.Stairs,
|
||||||
@@ -450,7 +503,7 @@ def hanger_from_door(door):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def connect_doors(a, b, world, player):
|
def connect_doors(a, b):
|
||||||
# Return on unsupported types.
|
# Return on unsupported types.
|
||||||
if a.type in [DoorType.Open, DoorType.StraightStairs, DoorType.Hole, DoorType.Warp, DoorType.Ladder,
|
if a.type in [DoorType.Open, DoorType.StraightStairs, DoorType.Hole, DoorType.Warp, DoorType.Ladder,
|
||||||
DoorType.Interior, DoorType.Logical]:
|
DoorType.Interior, DoorType.Logical]:
|
||||||
@@ -458,28 +511,26 @@ def connect_doors(a, b, world, player):
|
|||||||
# Connect supported types
|
# Connect supported types
|
||||||
if a.type == DoorType.Normal or a.type == DoorType.SpiralStairs:
|
if a.type == DoorType.Normal or a.type == DoorType.SpiralStairs:
|
||||||
if a.blocked:
|
if a.blocked:
|
||||||
connect_one_way(world, b.name, a.name, player)
|
connect_one_way(b.entrance, a.entrance)
|
||||||
elif b.blocked:
|
elif b.blocked:
|
||||||
connect_one_way(world, a.name, b.name, player)
|
connect_one_way(a.entrance, b.entrance)
|
||||||
else:
|
else:
|
||||||
connect_two_way(world, a.name, b.name, player)
|
connect_two_way(a.entrance, b.entrance)
|
||||||
dep_doors, target = [], None
|
dep_doors, target = [], None
|
||||||
if len(a.dependents) > 0:
|
if len(a.dependents) > 0:
|
||||||
dep_doors, target = a.dependents, b
|
dep_doors, target = a.dependents, b
|
||||||
elif len(b.dependents) > 0:
|
elif len(b.dependents) > 0:
|
||||||
dep_doors, target = b.dependents, a
|
dep_doors, target = b.dependents, a
|
||||||
if target is not None:
|
if target is not None:
|
||||||
target_region = world.get_entrance(target.name, player).parent_region
|
target_region = target.entrance.parent_region
|
||||||
for dep in dep_doors:
|
for dep in dep_doors:
|
||||||
connect_simple_door(dep, target_region, world, player)
|
connect_simple_door(dep, target_region)
|
||||||
return
|
return
|
||||||
# If we failed to account for a type, panic
|
# If we failed to account for a type, panic
|
||||||
raise RuntimeError('Unknown door type ' + a.type.name)
|
raise RuntimeError('Unknown door type ' + a.type.name)
|
||||||
|
|
||||||
|
|
||||||
def connect_two_way(world, entrancename, exitname, player):
|
def connect_two_way(entrance, ext):
|
||||||
entrance = world.get_entrance(entrancename, player)
|
|
||||||
ext = world.get_entrance(exitname, player)
|
|
||||||
|
|
||||||
# if these were already connected somewhere, remove the backreference
|
# if these were already connected somewhere, remove the backreference
|
||||||
if entrance.connected_region is not None:
|
if entrance.connected_region is not None:
|
||||||
@@ -491,17 +542,15 @@ def connect_two_way(world, entrancename, exitname, player):
|
|||||||
ext.connect(entrance.parent_region)
|
ext.connect(entrance.parent_region)
|
||||||
if entrance.parent_region.dungeon:
|
if entrance.parent_region.dungeon:
|
||||||
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||||
x = world.check_for_door(entrancename, player)
|
x = entrance.door
|
||||||
y = world.check_for_door(exitname, player)
|
y = ext.door
|
||||||
if x is not None:
|
if x is not None:
|
||||||
x.dest = y
|
x.dest = y
|
||||||
if y is not None:
|
if y is not None:
|
||||||
y.dest = x
|
y.dest = x
|
||||||
|
|
||||||
|
|
||||||
def connect_one_way(world, entrancename, exitname, player):
|
def connect_one_way(entrance, ext):
|
||||||
entrance = world.get_entrance(entrancename, player)
|
|
||||||
ext = world.get_entrance(exitname, player)
|
|
||||||
|
|
||||||
# if these were already connected somewhere, remove the backreference
|
# if these were already connected somewhere, remove the backreference
|
||||||
if entrance.connected_region is not None:
|
if entrance.connected_region is not None:
|
||||||
@@ -512,16 +561,16 @@ def connect_one_way(world, entrancename, exitname, player):
|
|||||||
entrance.connect(ext.parent_region)
|
entrance.connect(ext.parent_region)
|
||||||
if entrance.parent_region.dungeon:
|
if entrance.parent_region.dungeon:
|
||||||
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||||
x = world.check_for_door(entrancename, player)
|
x = entrance.door
|
||||||
y = world.check_for_door(exitname, player)
|
y = ext.door
|
||||||
if x is not None:
|
if x is not None:
|
||||||
x.dest = y
|
x.dest = y
|
||||||
if y is not None:
|
if y is not None:
|
||||||
y.dest = x
|
y.dest = x
|
||||||
|
|
||||||
|
|
||||||
def connect_simple_door(exit_door, region, world, player):
|
def connect_simple_door(exit_door, region):
|
||||||
world.get_entrance(exit_door.name, player).connect(region)
|
exit_door.entrance.connect(region)
|
||||||
exit_door.dest = region
|
exit_door.dest = region
|
||||||
|
|
||||||
|
|
||||||
@@ -679,7 +728,7 @@ class ExplorationState(object):
|
|||||||
if self.can_traverse(door):
|
if self.can_traverse(door):
|
||||||
if door.controller is not None:
|
if door.controller is not None:
|
||||||
door = door.controller
|
door = door.controller
|
||||||
if door.dest is None and door not in proposed_map.keys() and door in valid_doors:
|
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors.keys():
|
||||||
if not self.in_door_list_ic(door, self.unattached_doors):
|
if not self.in_door_list_ic(door, self.unattached_doors):
|
||||||
self.append_door_to_list(door, self.unattached_doors, flag)
|
self.append_door_to_list(door, self.unattached_doors, flag)
|
||||||
else:
|
else:
|
||||||
@@ -961,7 +1010,7 @@ def create_dungeon_builders(all_sectors, world, player, dungeon_entrances=None):
|
|||||||
current_dungeon = dungeon_map[key]
|
current_dungeon = dungeon_map[key]
|
||||||
for r_name in dungeon_boss_sectors[key]:
|
for r_name in dungeon_boss_sectors[key]:
|
||||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
|
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
|
||||||
if key == 'Hyrule Castle' and world.mode == 'standard':
|
if key == 'Hyrule Castle' and world.mode[player] == 'standard':
|
||||||
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
||||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
|
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors)
|
||||||
for key in dungeon_entrances.keys():
|
for key in dungeon_entrances.keys():
|
||||||
|
|||||||
@@ -278,8 +278,10 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
|
||||||
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
|
||||||
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||||
|
parser.add_argument('--remote_items', default=defval(False), action='store_true')
|
||||||
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
|
||||||
parser.add_argument('--names', default=defval(''))
|
parser.add_argument('--names', default=defval(''))
|
||||||
|
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||||
parser.add_argument('--outputpath')
|
parser.add_argument('--outputpath')
|
||||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||||
parser.add_argument('--outputname')
|
parser.add_argument('--outputname')
|
||||||
@@ -302,7 +304,8 @@ def parse_arguments(argv, no_defaults=False):
|
|||||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||||
'retro', 'accessibility', 'hints', 'beemizer',
|
'retro', 'accessibility', 'hints', 'beemizer',
|
||||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep']:
|
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
|
||||||
|
'remote_items']:
|
||||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||||
if player == 1:
|
if player == 1:
|
||||||
setattr(ret, name, {1: value})
|
setattr(ret, name, {1: value})
|
||||||
|
|||||||
+1
-1
@@ -350,7 +350,7 @@ flexible_starts = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drop_entrances = [
|
drop_entrances = [
|
||||||
'Sewers Rat Path', 'Skull Pinball', 'Skull Left Drop' # Pot circle, Back drop have unique access
|
'Sewers Rat Path', 'Skull Pinball', 'Skull Left Drop', 'Sanctuary' # Pot circle, Back drop have unique access
|
||||||
]
|
]
|
||||||
|
|
||||||
dungeon_keys = {
|
dungeon_keys = {
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||||||
fill_locations.reverse()
|
fill_locations.reverse()
|
||||||
|
|
||||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||||
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.mode[item.player] == 'standard' and world.keyshuffle[item.player] else 0)
|
# todo: crossed
|
||||||
|
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' and world.keyshuffle[item.player] and world.mode[item.player] == 'standard' else 0)
|
||||||
|
|
||||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||||
|
|
||||||
@@ -346,7 +347,7 @@ def balance_multiworld_progression(world):
|
|||||||
|
|
||||||
def get_sphere_locations(sphere_state, locations):
|
def get_sphere_locations(sphere_state, locations):
|
||||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||||
return [loc for loc in locations if sphere_state.can_reach(loc)]
|
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from DungeonRandomizer import parse_arguments
|
|||||||
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
||||||
from Main import main, __version__ as ESVersion
|
from Main import main, __version__ as ESVersion
|
||||||
from Rom import Sprite
|
from Rom import Sprite
|
||||||
from Utils import is_bundled, local_path, output_path, open_file, parse_names_string
|
from Utils import is_bundled, local_path, output_path, open_file
|
||||||
|
|
||||||
|
|
||||||
def guiMain(args=None):
|
def guiMain(args=None):
|
||||||
@@ -459,6 +459,7 @@ def guiMain(args=None):
|
|||||||
guiargs.heartcolor = heartcolorVar.get()
|
guiargs.heartcolor = heartcolorVar.get()
|
||||||
guiargs.fastmenu = fastMenuVar.get()
|
guiargs.fastmenu = fastMenuVar.get()
|
||||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||||
|
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
||||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
guiargs.suppress_rom = bool(suppressRomVar.get())
|
||||||
guiargs.openpyramid = bool(openpyramidVar.get())
|
guiargs.openpyramid = bool(openpyramidVar.get())
|
||||||
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
||||||
@@ -508,11 +509,7 @@ def guiMain(args=None):
|
|||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||||
else:
|
else:
|
||||||
msgtxt = "Rom patched successfully"
|
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||||
if guiargs.names:
|
|
||||||
for player, name in parse_names_string(guiargs.names).items():
|
|
||||||
msgtxt += "\nPlayer %d => %s" % (player, name)
|
|
||||||
messagebox.showinfo(title="Success", message=msgtxt)
|
|
||||||
|
|
||||||
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
||||||
|
|
||||||
@@ -612,20 +609,11 @@ def guiMain(args=None):
|
|||||||
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
uwPalettesLabel2 = Label(uwPalettesFrame2, text='Dungeon palettes')
|
||||||
uwPalettesLabel2.pack(side=LEFT)
|
uwPalettesLabel2.pack(side=LEFT)
|
||||||
|
|
||||||
namesFrame2 = Frame(drowDownFrame2)
|
|
||||||
namesLabel2 = Label(namesFrame2, text='Player names')
|
|
||||||
namesVar2 = StringVar()
|
|
||||||
namesEntry2 = Entry(namesFrame2, textvariable=namesVar2)
|
|
||||||
|
|
||||||
namesLabel2.pack(side=LEFT)
|
|
||||||
namesEntry2.pack(side=LEFT)
|
|
||||||
|
|
||||||
heartbeepFrame2.pack(expand=True, anchor=E)
|
heartbeepFrame2.pack(expand=True, anchor=E)
|
||||||
heartcolorFrame2.pack(expand=True, anchor=E)
|
heartcolorFrame2.pack(expand=True, anchor=E)
|
||||||
fastMenuFrame2.pack(expand=True, anchor=E)
|
fastMenuFrame2.pack(expand=True, anchor=E)
|
||||||
owPalettesFrame2.pack(expand=True, anchor=E)
|
owPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
uwPalettesFrame2.pack(expand=True, anchor=E)
|
uwPalettesFrame2.pack(expand=True, anchor=E)
|
||||||
namesFrame2.pack(expand=True, anchor=E)
|
|
||||||
|
|
||||||
bottomFrame2 = Frame(topFrame2)
|
bottomFrame2 = Frame(topFrame2)
|
||||||
|
|
||||||
@@ -641,18 +629,13 @@ def guiMain(args=None):
|
|||||||
guiargs.rom = romVar2.get()
|
guiargs.rom = romVar2.get()
|
||||||
guiargs.baserom = romVar.get()
|
guiargs.baserom = romVar.get()
|
||||||
guiargs.sprite = sprite
|
guiargs.sprite = sprite
|
||||||
guiargs.names = namesEntry2.get()
|
|
||||||
try:
|
try:
|
||||||
adjust(args=guiargs)
|
adjust(args=guiargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
messagebox.showerror(title="Error while creating seed", message=str(e))
|
messagebox.showerror(title="Error while creating seed", message=str(e))
|
||||||
else:
|
else:
|
||||||
msgtxt = "Rom patched successfully"
|
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||||
if guiargs.names:
|
|
||||||
for player, name in parse_names_string(guiargs.names).items():
|
|
||||||
msgtxt += "\nPlayer %d => %s" % (player, name)
|
|
||||||
messagebox.showinfo(title="Success", message=msgtxt)
|
|
||||||
|
|
||||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,15 @@ def generate_itempool(world, player):
|
|||||||
world.push_item(world.get_location('Ice Block Drop', player), ItemFactory('Convenient Block', player), False)
|
world.push_item(world.get_location('Ice Block Drop', player), ItemFactory('Convenient Block', player), False)
|
||||||
world.get_location('Ice Block Drop', player).event = True
|
world.get_location('Ice Block Drop', player).event = True
|
||||||
world.get_location('Ice Block Drop', player).locked = True
|
world.get_location('Ice Block Drop', player).locked = True
|
||||||
|
# if world.mode[player] == 'standard': todo: multi
|
||||||
|
if world.mode[player] == 'standard':
|
||||||
|
world.push_item(world.get_location('Zelda Pickup', player), ItemFactory('Zelda Herself', player), False)
|
||||||
|
world.get_location('Zelda Pickup', player).event = True
|
||||||
|
world.get_location('Zelda Pickup', player).locked = True
|
||||||
|
world.push_item(world.get_location('Zelda Drop Off', player), ItemFactory('Zelda Delivered', player), False)
|
||||||
|
world.get_location('Zelda Drop Off', player).event = True
|
||||||
|
world.get_location('Zelda Drop Off', player).locked = True
|
||||||
|
|
||||||
|
|
||||||
# set up item pool
|
# set up item pool
|
||||||
if world.custom:
|
if world.custom:
|
||||||
|
|||||||
@@ -183,4 +183,6 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||||||
'Maiden Rescued': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
'Maiden Rescued': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||||
'Maiden Unmasked': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
'Maiden Unmasked': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||||
'Convenient Block': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
'Convenient Block': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||||
|
'Zelda Herself': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||||
|
'Zelda Delivered': (True, False, 'Event', None, None, None, None, None, None, None, None),
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-10
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
|
||||||
from Regions import dungeon_events
|
from Regions import dungeon_events
|
||||||
@@ -46,8 +47,9 @@ class KeyLogic(object):
|
|||||||
|
|
||||||
class DoorRules(object):
|
class DoorRules(object):
|
||||||
|
|
||||||
def __init__(self, number):
|
def __init__(self, number, is_valid):
|
||||||
self.small_key_num = number
|
self.small_key_num = number
|
||||||
|
self.is_valid = is_valid
|
||||||
# allowing a different number if bk is behind this door in a set of locations
|
# allowing a different number if bk is behind this door in a set of locations
|
||||||
self.alternate_small_key = None
|
self.alternate_small_key = None
|
||||||
self.alternate_big_key_loc = set()
|
self.alternate_big_key_loc = set()
|
||||||
@@ -144,6 +146,8 @@ def analyze_dungeon(key_layout, world, player):
|
|||||||
if not child.bigKey and child not in doors_completed:
|
if not child.bigKey and child not in doors_completed:
|
||||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
|
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
|
||||||
rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
||||||
|
if not rule.is_valid:
|
||||||
|
logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name)
|
||||||
if smallest_rule is None or rule.small_key_num < smallest_rule:
|
if smallest_rule is None or rule.small_key_num < smallest_rule:
|
||||||
smallest_rule = rule.small_key_num
|
smallest_rule = rule.small_key_num
|
||||||
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
||||||
@@ -163,6 +167,14 @@ def analyze_dungeon(key_layout, world, player):
|
|||||||
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
|
key_logic.sm_restricted.update(find_big_chest_locations(key_counter.free_locations))
|
||||||
check_rules(original_key_counter, key_layout, world, player)
|
check_rules(original_key_counter, key_layout, world, player)
|
||||||
|
|
||||||
|
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
|
||||||
|
for rule in key_logic.door_rules.values():
|
||||||
|
if rule.alternate_small_key is not None and rule.alternate_small_key > rule.small_key_num:
|
||||||
|
max_counter = find_max_counter(key_layout)
|
||||||
|
rule.alternate_big_key_loc = set(max_counter.free_locations.keys()).difference(rule.alternate_big_key_loc)
|
||||||
|
rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def count_key_drops(sector):
|
def count_key_drops(sector):
|
||||||
cnt = 0
|
cnt = 0
|
||||||
@@ -343,9 +355,10 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
|
|||||||
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
|
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
|
||||||
adj_chest_keys = min(chest_keys, required_keys)
|
adj_chest_keys = min(chest_keys, required_keys)
|
||||||
needed_chests = required_keys - len(key_counter.key_only_locations)
|
needed_chests = required_keys - len(key_counter.key_only_locations)
|
||||||
|
is_valid = needed_chests <= chest_keys
|
||||||
unneeded_chests = min(key_gain, adj_chest_keys - needed_chests)
|
unneeded_chests = min(key_gain, adj_chest_keys - needed_chests)
|
||||||
rule_num = required_keys - unneeded_chests
|
rule_num = required_keys - unneeded_chests
|
||||||
return DoorRules(rule_num)
|
return DoorRules(rule_num, is_valid)
|
||||||
|
|
||||||
|
|
||||||
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
|
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
|
||||||
@@ -451,8 +464,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
|
|||||||
if key_counter.big_key_opened:
|
if key_counter.big_key_opened:
|
||||||
return
|
return
|
||||||
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
|
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
|
||||||
bk_number = create_rule(best_counter, key_counter, key_layout, world, player).small_key_num
|
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
||||||
if bk_number == rule.small_key_num:
|
if bk_rule.is_valid and bk_rule.small_key_num == rule.small_key_num:
|
||||||
return
|
return
|
||||||
door_open = find_next_counter(door, best_counter, key_layout)
|
door_open = find_next_counter(door, best_counter, key_layout)
|
||||||
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
|
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
|
||||||
@@ -463,10 +476,11 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
|
|||||||
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
|
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
|
||||||
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
|
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
|
||||||
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
|
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
|
||||||
if len(unique_loc) > 0:
|
if bk_rule.is_valid and len(unique_loc) > 0:
|
||||||
rule.alternate_small_key = bk_number
|
rule.alternate_small_key = bk_rule.small_key_num
|
||||||
rule.alternate_big_key_loc.update(unique_loc)
|
rule.alternate_big_key_loc.update(unique_loc)
|
||||||
|
elif not bk_rule.is_valid:
|
||||||
|
key_layout.key_logic.bk_restricted.update(unique_loc)
|
||||||
|
|
||||||
def open_a_door(door, child_state, flat_proposal):
|
def open_a_door(door, child_state, flat_proposal):
|
||||||
if door.bigKey:
|
if door.bigKey:
|
||||||
@@ -604,7 +618,7 @@ def check_rules(original_counter, key_layout, world, player):
|
|||||||
access_rules = key_only_map[loc]
|
access_rules = key_only_map[loc]
|
||||||
if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
|
if access_door is None or access_door.name not in key_layout.key_logic.door_rules.keys():
|
||||||
if access_door is None or not access_door.bigKey:
|
if access_door is None or not access_door.bigKey:
|
||||||
access_rules.append(DoorRules(0))
|
access_rules.append(DoorRules(0, True))
|
||||||
else:
|
else:
|
||||||
rule = key_layout.key_logic.door_rules[access_door.name]
|
rule = key_layout.key_logic.door_rules[access_door.name]
|
||||||
if rule not in access_rules:
|
if rule not in access_rules:
|
||||||
@@ -1000,11 +1014,15 @@ def validate_vanilla_key_logic(world, player):
|
|||||||
|
|
||||||
|
|
||||||
def val_hyrule(key_logic, world, player):
|
def val_hyrule(key_logic, world, player):
|
||||||
|
if world.mode[player] == 'standard':
|
||||||
|
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1)
|
||||||
|
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2)
|
||||||
|
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
|
||||||
|
val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4)
|
||||||
|
else:
|
||||||
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
|
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
|
||||||
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
|
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
|
||||||
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
|
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 2)
|
||||||
# why is allow_small actually false? - because chest key is forced elsewhere?
|
|
||||||
# val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 3, True, 'Hyrule Castle - Zelda\'s Chest')
|
|
||||||
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
|
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 4)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from Items import ItemFactory
|
|||||||
from Regions import create_regions, create_shops, mark_light_world_regions
|
from Regions import create_regions, create_shops, mark_light_world_regions
|
||||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||||
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom
|
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
|
||||||
from Doors import create_doors
|
from Doors import create_doors
|
||||||
from DoorShuffle import link_doors
|
from DoorShuffle import link_doors
|
||||||
from RoomData import create_rooms
|
from RoomData import create_rooms
|
||||||
@@ -21,9 +21,9 @@ from Rules import set_rules
|
|||||||
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
from Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
|
||||||
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
|
from Fill import distribute_items_cutoff, distribute_items_staleness, distribute_items_restrictive, flood_items, balance_multiworld_progression
|
||||||
from ItemList import generate_itempool, difficulties, fill_prizes
|
from ItemList import generate_itempool, difficulties, fill_prizes
|
||||||
from Utils import output_path, parse_names_string
|
from Utils import output_path, parse_player_names
|
||||||
|
|
||||||
__version__ = '0.0.1-pre'
|
__version__ = '0.0.2-pre'
|
||||||
|
|
||||||
def main(args, seed=None):
|
def main(args, seed=None):
|
||||||
if args.outputpath:
|
if args.outputpath:
|
||||||
@@ -42,6 +42,7 @@ def main(args, seed=None):
|
|||||||
world.seed = int(seed)
|
world.seed = int(seed)
|
||||||
random.seed(world.seed)
|
random.seed(world.seed)
|
||||||
|
|
||||||
|
world.remote_items = args.remote_items.copy()
|
||||||
world.mapshuffle = args.mapshuffle.copy()
|
world.mapshuffle = args.mapshuffle.copy()
|
||||||
world.compassshuffle = args.compassshuffle.copy()
|
world.compassshuffle = args.compassshuffle.copy()
|
||||||
world.keyshuffle = args.keyshuffle.copy()
|
world.keyshuffle = args.keyshuffle.copy()
|
||||||
@@ -57,7 +58,16 @@ def main(args, seed=None):
|
|||||||
|
|
||||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
logger.info('ALttP Door Randomizer Version %s - Seed: %s\n\n', __version__, world.seed)
|
logger.info('ALttP Door Randomizer Version %s - Seed: %s\n', __version__, world.seed)
|
||||||
|
|
||||||
|
parsed_names = parse_player_names(args.names, world.players, args.teams)
|
||||||
|
world.teams = len(parsed_names)
|
||||||
|
for i, team in enumerate(parsed_names, 1):
|
||||||
|
if world.players > 1:
|
||||||
|
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
|
||||||
|
for player, name in enumerate(team, 1):
|
||||||
|
world.player_names[player].append(name)
|
||||||
|
logger.info('')
|
||||||
|
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||||
@@ -142,14 +152,18 @@ def main(args, seed=None):
|
|||||||
logger.info('Balancing multiworld progression.')
|
logger.info('Balancing multiworld progression.')
|
||||||
balance_multiworld_progression(world)
|
balance_multiworld_progression(world)
|
||||||
|
|
||||||
|
# if we only check for beatable, we can do this sanity check first before creating the rom
|
||||||
|
if not world.can_beat_game():
|
||||||
|
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
|
||||||
|
|
||||||
logger.info('Patching ROM.')
|
logger.info('Patching ROM.')
|
||||||
|
|
||||||
player_names = parse_names_string(args.names)
|
|
||||||
outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed)
|
outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed)
|
||||||
|
|
||||||
rom_names = []
|
rom_names = []
|
||||||
jsonout = {}
|
jsonout = {}
|
||||||
if not args.suppress_rom:
|
if not args.suppress_rom:
|
||||||
|
for team in range(world.teams):
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
||||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] != 'none'
|
||||||
@@ -158,23 +172,23 @@ def main(args, seed=None):
|
|||||||
|
|
||||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
||||||
|
|
||||||
patch_rom(world, player, rom, use_enemizer)
|
patch_rom(world, rom, player, team, use_enemizer)
|
||||||
rom_names.append((player, list(rom.name)))
|
|
||||||
|
|
||||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||||
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
|
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
|
||||||
if not args.jsonout:
|
if not args.jsonout:
|
||||||
patches = rom.patches
|
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
||||||
rom = LocalRom(args.rom)
|
|
||||||
rom.merge_enemizer_patches(patches)
|
|
||||||
|
|
||||||
if args.race:
|
if args.race:
|
||||||
patch_race_rom(rom)
|
patch_race_rom(rom)
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player], player_names)
|
rom_names.append((player, team, list(rom.name)))
|
||||||
|
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
|
||||||
|
|
||||||
|
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player], args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.ow_palettes[player], args.uw_palettes[player])
|
||||||
|
|
||||||
if args.jsonout:
|
if args.jsonout:
|
||||||
jsonout[f'patch{player}'] = rom.patches
|
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||||
else:
|
else:
|
||||||
mcsb_name = ''
|
mcsb_name = ''
|
||||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]):
|
||||||
@@ -186,20 +200,26 @@ def main(args, seed=None):
|
|||||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||||
|
|
||||||
playername = f"{f'_P{player}' if world.players > 1 else ''}{f'_{player_names[player]}' if player in player_names else ''}"
|
outfilepname = f'_T{team+1}' if world.teams > 1 else ''
|
||||||
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
|
if world.players > 1:
|
||||||
|
outfilepname += f'_P{player}'
|
||||||
|
if world.players > 1 or world.teams > 1:
|
||||||
|
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else ''
|
||||||
|
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic[player], world.difficulty[player], world.difficulty_adjustments[player],
|
||||||
world.mode[player], world.goal[player],
|
world.mode[player], world.goal[player],
|
||||||
"" if world.timer in ['none', 'display'] else "-" + world.timer,
|
"" if world.timer in ['none', 'display'] else "-" + world.timer,
|
||||||
world.shuffle[player], world.doorShuffle[player], world.algorithm, mcsb_name,
|
world.shuffle[player], world.algorithm, mcsb_name,
|
||||||
"-retro" if world.retro[player] else "",
|
"-retro" if world.retro[player] else "",
|
||||||
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
|
"-prog_" + world.progressive if world.progressive in ['off', 'random'] else "",
|
||||||
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
"-nohints" if not world.hints[player] else "")) if not args.outputname else ''
|
||||||
rom.write_to_file(output_path(f'{outfilebase}{playername}{outfilesuffix}.sfc'))
|
rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc'))
|
||||||
|
|
||||||
multidata = zlib.compress(json.dumps((world.players,
|
multidata = zlib.compress(json.dumps({"names": parsed_names,
|
||||||
rom_names,
|
"roms": rom_names,
|
||||||
[((location.address, location.player), (location.item.code, location.item.player)) for location in world.get_filled_locations() if type(location.address) is int])
|
"remote_items": [player for player in range(1, world.players + 1) if world.remote_items[player]],
|
||||||
).encode("utf-8"))
|
"locations": [((location.address, location.player), (location.item.code, location.item.player))
|
||||||
|
for location in world.get_filled_locations() if type(location.address) is int]
|
||||||
|
}).encode("utf-8"))
|
||||||
if args.jsonout:
|
if args.jsonout:
|
||||||
jsonout["multidata"] = list(multidata)
|
jsonout["multidata"] = list(multidata)
|
||||||
else:
|
else:
|
||||||
@@ -219,13 +239,16 @@ def main(args, seed=None):
|
|||||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||||
|
|
||||||
logger.info('Done. Enjoy.')
|
logger.info('Done. Enjoy.')
|
||||||
logger.debug('Total Time: %s', time.perf_counter() - start)
|
logger.info('Total Time: %s', time.perf_counter() - start)
|
||||||
|
|
||||||
return world
|
return world
|
||||||
|
|
||||||
def copy_world(world):
|
def copy_world(world):
|
||||||
# ToDo: Not good yet
|
# ToDo: Not good yet
|
||||||
ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||||
|
ret.teams = world.teams
|
||||||
|
ret.player_names = copy.deepcopy(world.player_names)
|
||||||
|
ret.remote_items = world.remote_items.copy()
|
||||||
ret.required_medallions = world.required_medallions.copy()
|
ret.required_medallions = world.required_medallions.copy()
|
||||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||||
@@ -293,6 +316,7 @@ def copy_world(world):
|
|||||||
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
|
item = Item(location.item.name, location.item.advancement, location.item.priority, location.item.type, player = location.item.player)
|
||||||
ret.get_location(location.name, location.player).item = item
|
ret.get_location(location.name, location.player).item = item
|
||||||
item.location = ret.get_location(location.name, location.player)
|
item.location = ret.get_location(location.name, location.player)
|
||||||
|
item.world = ret
|
||||||
if location.event:
|
if location.event:
|
||||||
ret.get_location(location.name, location.player).event = True
|
ret.get_location(location.name, location.player).event = True
|
||||||
if location.locked:
|
if location.locked:
|
||||||
@@ -302,9 +326,11 @@ def copy_world(world):
|
|||||||
for item in world.itempool:
|
for item in world.itempool:
|
||||||
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
|
ret.itempool.append(Item(item.name, item.advancement, item.priority, item.type, player = item.player))
|
||||||
|
|
||||||
|
for item in world.precollected_items:
|
||||||
|
ret.push_precollected(ItemFactory(item.name, item.player))
|
||||||
|
|
||||||
# copy progress items in state
|
# copy progress items in state
|
||||||
ret.state.prog_items = world.state.prog_items.copy()
|
ret.state.prog_items = world.state.prog_items.copy()
|
||||||
ret.precollected_items = world.precollected_items.copy()
|
|
||||||
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
ret.state.stale = {player: True for player in range(1, world.players + 1)}
|
||||||
|
|
||||||
ret.doors = world.doors
|
ret.doors = world.doors
|
||||||
@@ -350,10 +376,6 @@ def create_playthrough(world):
|
|||||||
old_world = world
|
old_world = world
|
||||||
world = copy_world(world)
|
world = copy_world(world)
|
||||||
|
|
||||||
# if we only check for beatable, we can do this sanity check first before writing down spheres
|
|
||||||
if not world.can_beat_game():
|
|
||||||
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
|
|
||||||
|
|
||||||
# get locations containing progress items
|
# get locations containing progress items
|
||||||
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
|
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
|
||||||
state_cache = [None]
|
state_cache = [None]
|
||||||
|
|||||||
+211
-195
@@ -1,58 +1,38 @@
|
|||||||
|
import aioconsole
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import colorama
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import shlex
|
||||||
import subprocess
|
import urllib.parse
|
||||||
import sys
|
import websockets
|
||||||
|
|
||||||
import Items
|
import Items
|
||||||
import Regions
|
import Regions
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
import aioconsole
|
|
||||||
break
|
|
||||||
except ImportError:
|
|
||||||
aioconsole = None
|
|
||||||
print('Required python module "aioconsole" not found, press enter to install it')
|
|
||||||
input()
|
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'aioconsole'])
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
import websockets
|
|
||||||
break
|
|
||||||
except ImportError:
|
|
||||||
websockets = None
|
|
||||||
print('Required python module "websockets" not found, press enter to install it')
|
|
||||||
input()
|
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'websockets'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
import colorama
|
|
||||||
except ImportError:
|
|
||||||
colorama = None
|
|
||||||
|
|
||||||
class ReceivedItem:
|
class ReceivedItem:
|
||||||
def __init__(self, item, location, player_id, player_name):
|
def __init__(self, item, location, player):
|
||||||
self.item = item
|
self.item = item
|
||||||
self.location = location
|
self.location = location
|
||||||
self.player_id = player_id
|
self.player = player
|
||||||
self.player_name = player_name
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
def __init__(self, snes_address, server_address, password, name, team, slot):
|
def __init__(self, snes_address, server_address, password):
|
||||||
self.snes_address = snes_address
|
self.snes_address = snes_address
|
||||||
self.server_address = server_address
|
self.server_address = server_address
|
||||||
|
|
||||||
self.exit_event = asyncio.Event()
|
self.exit_event = asyncio.Event()
|
||||||
|
self.watcher_event = asyncio.Event()
|
||||||
|
|
||||||
self.input_queue = asyncio.Queue()
|
self.input_queue = asyncio.Queue()
|
||||||
self.input_requests = 0
|
self.input_requests = 0
|
||||||
|
|
||||||
self.snes_socket = None
|
self.snes_socket = None
|
||||||
self.snes_state = SNES_DISCONNECTED
|
self.snes_state = SNES_DISCONNECTED
|
||||||
|
self.snes_attached_device = None
|
||||||
|
self.snes_reconnect_address = None
|
||||||
self.snes_recv_queue = asyncio.Queue()
|
self.snes_recv_queue = asyncio.Queue()
|
||||||
self.snes_request_lock = asyncio.Lock()
|
self.snes_request_lock = asyncio.Lock()
|
||||||
self.is_sd2snes = False
|
self.is_sd2snes = False
|
||||||
@@ -62,15 +42,16 @@ class Context:
|
|||||||
self.socket = None
|
self.socket = None
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
self.name = name
|
self.team = None
|
||||||
self.team = team
|
self.slot = None
|
||||||
self.slot = slot
|
self.player_names = {}
|
||||||
|
|
||||||
self.locations_checked = set()
|
self.locations_checked = set()
|
||||||
|
self.locations_scouted = set()
|
||||||
self.items_received = []
|
self.items_received = []
|
||||||
self.last_rom = None
|
self.locations_info = {}
|
||||||
self.expected_rom = None
|
self.awaiting_rom = False
|
||||||
self.rom_confirmed = False
|
self.rom = None
|
||||||
|
self.auth = None
|
||||||
|
|
||||||
def color_code(*args):
|
def color_code(*args):
|
||||||
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
@@ -81,6 +62,7 @@ def color_code(*args):
|
|||||||
def color(text, *args):
|
def color(text, *args):
|
||||||
return color_code(*args) + text + color_code('reset')
|
return color_code(*args) + text + color_code('reset')
|
||||||
|
|
||||||
|
RECONNECT_DELAY = 30
|
||||||
|
|
||||||
ROM_START = 0x000000
|
ROM_START = 0x000000
|
||||||
WRAM_START = 0xF50000
|
WRAM_START = 0xF50000
|
||||||
@@ -100,6 +82,10 @@ RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
|||||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||||
|
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
|
||||||
|
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
|
||||||
|
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||||
|
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||||
|
|
||||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
"Blind's Hideout - Left": (0x11d, 0x20),
|
"Blind's Hideout - Left": (0x11d, 0x20),
|
||||||
@@ -323,18 +309,17 @@ SNES_CONNECTING = 1
|
|||||||
SNES_CONNECTED = 2
|
SNES_CONNECTED = 2
|
||||||
SNES_ATTACHED = 3
|
SNES_ATTACHED = 3
|
||||||
|
|
||||||
async def snes_connect(ctx : Context, address = None):
|
async def snes_connect(ctx : Context, address):
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
print('Already connected to snes')
|
logging.error('Already connected to snes')
|
||||||
return
|
return
|
||||||
|
|
||||||
ctx.snes_state = SNES_CONNECTING
|
ctx.snes_state = SNES_CONNECTING
|
||||||
recv_task = None
|
recv_task = None
|
||||||
|
|
||||||
if address is None:
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
address = 'ws://' + ctx.snes_address
|
|
||||||
|
|
||||||
print("Connecting to QUsb2snes at %s ..." % address)
|
logging.info("Connecting to QUsb2snes at %s ..." % address)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
|
ctx.snes_socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
|
||||||
@@ -352,24 +337,32 @@ async def snes_connect(ctx : Context, address = None):
|
|||||||
if not devices:
|
if not devices:
|
||||||
raise Exception('No device found')
|
raise Exception('No device found')
|
||||||
|
|
||||||
print("Available devices:")
|
logging.info("Available devices:")
|
||||||
for id, device in enumerate(devices):
|
for id, device in enumerate(devices):
|
||||||
print("[%d] %s" % (id + 1, device))
|
logging.info("[%d] %s" % (id + 1, device))
|
||||||
|
|
||||||
device = None
|
device = None
|
||||||
|
if len(devices) == 1:
|
||||||
|
device = devices[0]
|
||||||
|
elif ctx.snes_reconnect_address:
|
||||||
|
if ctx.snes_attached_device[1] in devices:
|
||||||
|
device = ctx.snes_attached_device[1]
|
||||||
|
else:
|
||||||
|
device = devices[ctx.snes_attached_device[0]]
|
||||||
|
else:
|
||||||
while True:
|
while True:
|
||||||
print("Enter a number:")
|
logging.info("Select a device:")
|
||||||
choice = await console_input(ctx)
|
choice = await console_input(ctx)
|
||||||
if choice is None:
|
if choice is None:
|
||||||
raise Exception('Abort input')
|
raise Exception('Abort input')
|
||||||
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
|
if not choice.isdigit() or int(choice) < 1 or int(choice) > len(devices):
|
||||||
print("Invalid choice (%s)" % choice)
|
logging.warning("Invalid choice (%s)" % choice)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
device = devices[int(choice) - 1]
|
device = devices[int(choice) - 1]
|
||||||
break
|
break
|
||||||
|
|
||||||
print("Attaching to " + device)
|
logging.info("Attaching to " + device)
|
||||||
|
|
||||||
Attach_Request = {
|
Attach_Request = {
|
||||||
"Opcode" : "Attach",
|
"Opcode" : "Attach",
|
||||||
@@ -378,21 +371,22 @@ async def snes_connect(ctx : Context, address = None):
|
|||||||
}
|
}
|
||||||
await ctx.snes_socket.send(json.dumps(Attach_Request))
|
await ctx.snes_socket.send(json.dumps(Attach_Request))
|
||||||
ctx.snes_state = SNES_ATTACHED
|
ctx.snes_state = SNES_ATTACHED
|
||||||
|
ctx.snes_attached_device = (devices.index(device), device)
|
||||||
|
|
||||||
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||||
print("SD2SNES Detected")
|
logging.info("SD2SNES Detected")
|
||||||
ctx.is_sd2snes = True
|
ctx.is_sd2snes = True
|
||||||
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||||
reply = json.loads(await ctx.snes_socket.recv())
|
reply = json.loads(await ctx.snes_socket.recv())
|
||||||
if reply and 'Results' in reply:
|
if reply and 'Results' in reply:
|
||||||
print(reply['Results'])
|
logging.info(reply['Results'])
|
||||||
else:
|
else:
|
||||||
ctx.is_sd2snes = False
|
ctx.is_sd2snes = False
|
||||||
|
|
||||||
|
ctx.snes_reconnect_address = address
|
||||||
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
recv_task = asyncio.create_task(snes_recv_loop(ctx))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error connecting to snes (%s)" % e)
|
|
||||||
if recv_task is not None:
|
if recv_task is not None:
|
||||||
if not ctx.snes_socket.closed:
|
if not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
@@ -402,16 +396,26 @@ async def snes_connect(ctx : Context, address = None):
|
|||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
ctx.snes_socket = None
|
ctx.snes_socket = None
|
||||||
ctx.snes_state = SNES_DISCONNECTED
|
ctx.snes_state = SNES_DISCONNECTED
|
||||||
|
if not ctx.snes_reconnect_address:
|
||||||
|
logging.error("Error connecting to snes (%s)" % e)
|
||||||
|
else:
|
||||||
|
logging.error(f"Error connecting to snes, attempt again in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
|
|
||||||
|
async def snes_autoreconnect(ctx: Context):
|
||||||
|
await asyncio.sleep(RECONNECT_DELAY)
|
||||||
|
if ctx.snes_reconnect_address and ctx.snes_socket is None:
|
||||||
|
await snes_connect(ctx, ctx.snes_reconnect_address)
|
||||||
|
|
||||||
async def snes_recv_loop(ctx : Context):
|
async def snes_recv_loop(ctx : Context):
|
||||||
try:
|
try:
|
||||||
async for msg in ctx.snes_socket:
|
async for msg in ctx.snes_socket:
|
||||||
ctx.snes_recv_queue.put_nowait(msg)
|
ctx.snes_recv_queue.put_nowait(msg)
|
||||||
print("Snes disconnected, type /snes to reconnect")
|
logging.warning("Snes disconnected")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Lost connection to the snes, type /snes to reconnect")
|
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
|
logging.error("Lost connection to the snes, type /snes to reconnect")
|
||||||
finally:
|
finally:
|
||||||
socket, ctx.snes_socket = ctx.snes_socket, None
|
socket, ctx.snes_socket = ctx.snes_socket, None
|
||||||
if socket is not None and not socket.closed:
|
if socket is not None and not socket.closed:
|
||||||
@@ -421,8 +425,11 @@ async def snes_recv_loop(ctx : Context):
|
|||||||
ctx.snes_recv_queue = asyncio.Queue()
|
ctx.snes_recv_queue = asyncio.Queue()
|
||||||
ctx.hud_message_queue = []
|
ctx.hud_message_queue = []
|
||||||
|
|
||||||
ctx.rom_confirmed = False
|
ctx.rom = None
|
||||||
ctx.last_rom = None
|
|
||||||
|
if ctx.snes_reconnect_address:
|
||||||
|
logging.info(f"...reconnecting in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(snes_autoreconnect(ctx))
|
||||||
|
|
||||||
async def snes_read(ctx : Context, address, size):
|
async def snes_read(ctx : Context, address, size):
|
||||||
try:
|
try:
|
||||||
@@ -449,9 +456,9 @@ async def snes_read(ctx : Context, address, size):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if len(data) != size:
|
if len(data) != size:
|
||||||
print('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
logging.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
|
||||||
if len(data):
|
if len(data):
|
||||||
print(str(data))
|
logging.error(str(data))
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
return None
|
return None
|
||||||
@@ -477,7 +484,7 @@ async def snes_write(ctx : Context, write_list):
|
|||||||
|
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
if (address < WRAM_START) or ((address + len(data)) > (WRAM_START + WRAM_SIZE)):
|
||||||
print("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
logging.error("SD2SNES: Write out of range %s (%d)" % (hex(address), len(data)))
|
||||||
return False
|
return False
|
||||||
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
for ptr, byte in enumerate(data, address + 0x7E0000 - WRAM_START):
|
||||||
cmd += b'\xA9' # LDA
|
cmd += b'\xA9' # LDA
|
||||||
@@ -534,100 +541,100 @@ async def send_msgs(websocket, msgs):
|
|||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def server_loop(ctx : Context):
|
async def server_loop(ctx : Context, address = None):
|
||||||
if ctx.socket is not None:
|
if ctx.socket is not None:
|
||||||
print('Already connected')
|
logging.error('Already connected')
|
||||||
return
|
return
|
||||||
|
|
||||||
while not ctx.server_address:
|
if address is None:
|
||||||
print('Enter multiworld server address')
|
address = ctx.server_address
|
||||||
ctx.server_address = await console_input(ctx)
|
|
||||||
|
|
||||||
address = f"ws://{ctx.server_address}" if "://" not in ctx.server_address else ctx.server_address
|
while not address:
|
||||||
|
logging.info('Enter multiworld server address')
|
||||||
|
address = await console_input(ctx)
|
||||||
|
|
||||||
print('Connecting to multiworld server at %s' % address)
|
address = f"ws://{address}" if "://" not in address else address
|
||||||
|
port = urllib.parse.urlparse(address).port or 38281
|
||||||
|
|
||||||
|
logging.info('Connecting to multiworld server at %s' % address)
|
||||||
try:
|
try:
|
||||||
ctx.socket = await websockets.connect(address, ping_timeout=None, ping_interval=None)
|
ctx.socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||||
print('Connected')
|
logging.info('Connected')
|
||||||
|
ctx.server_address = address
|
||||||
|
|
||||||
async for data in ctx.socket:
|
async for data in ctx.socket:
|
||||||
for msg in json.loads(data):
|
for msg in json.loads(data):
|
||||||
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
|
cmd, args = (msg[0], msg[1]) if len(msg) > 1 else (msg, None)
|
||||||
await process_server_cmd(ctx, cmd, args)
|
await process_server_cmd(ctx, cmd, args)
|
||||||
print('Disconnected from multiworld server, type /connect to reconnect')
|
logging.warning('Disconnected from multiworld server, type /connect to reconnect')
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
print('Connection refused by the multiworld server')
|
logging.error('Connection refused by the multiworld server')
|
||||||
except (OSError, websockets.InvalidURI):
|
except (OSError, websockets.InvalidURI):
|
||||||
print('Failed to connect to the multiworld server')
|
logging.error('Failed to connect to the multiworld server')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Lost connection to the multiworld server, type /connect to reconnect')
|
logging.error('Lost connection to the multiworld server, type /connect to reconnect')
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
finally:
|
finally:
|
||||||
ctx.name = None
|
ctx.awaiting_rom = False
|
||||||
ctx.team = None
|
ctx.auth = None
|
||||||
ctx.slot = None
|
ctx.items_received = []
|
||||||
ctx.expected_rom = None
|
ctx.locations_info = {}
|
||||||
ctx.rom_confirmed = False
|
|
||||||
socket, ctx.socket = ctx.socket, None
|
socket, ctx.socket = ctx.socket, None
|
||||||
if socket is not None and not socket.closed:
|
if socket is not None and not socket.closed:
|
||||||
await socket.close()
|
await socket.close()
|
||||||
ctx.server_task = None
|
ctx.server_task = None
|
||||||
|
if ctx.server_address:
|
||||||
|
logging.info(f"... reconnecting in {RECONNECT_DELAY}s")
|
||||||
|
asyncio.create_task(server_autoreconnect(ctx))
|
||||||
|
|
||||||
|
async def server_autoreconnect(ctx: Context):
|
||||||
|
await asyncio.sleep(RECONNECT_DELAY)
|
||||||
|
if ctx.server_address and ctx.server_task is None:
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||||
|
|
||||||
async def process_server_cmd(ctx : Context, cmd, args):
|
async def process_server_cmd(ctx : Context, cmd, args):
|
||||||
if cmd == 'RoomInfo':
|
if cmd == 'RoomInfo':
|
||||||
print('--------------------------------')
|
logging.info('--------------------------------')
|
||||||
print('Room Information:')
|
logging.info('Room Information:')
|
||||||
print('--------------------------------')
|
logging.info('--------------------------------')
|
||||||
if args['password']:
|
if args['password']:
|
||||||
print('Password required')
|
logging.info('Password required')
|
||||||
print('%d players seed' % args['slots'])
|
|
||||||
if len(args['players']) < 1:
|
if len(args['players']) < 1:
|
||||||
print('No player connected')
|
logging.info('No player connected')
|
||||||
else:
|
else:
|
||||||
args['players'].sort(key=lambda player: ('' if not player[1] else player[1].lower(), player[2]))
|
args['players'].sort()
|
||||||
current_team = 0
|
current_team = 0
|
||||||
print('Connected players:')
|
logging.info('Connected players:')
|
||||||
for name, team, slot in args['players']:
|
logging.info(' Team #1')
|
||||||
|
for team, slot, name in args['players']:
|
||||||
if team != current_team:
|
if team != current_team:
|
||||||
print(' Default team' if not team else ' Team: %s' % team)
|
logging.info(f' Team #{team + 1}')
|
||||||
current_team = team
|
current_team = team
|
||||||
print(' %s (Player %d)' % (name, slot))
|
logging.info(' %s (Player %d)' % (name, slot))
|
||||||
await server_auth(ctx, args['password'])
|
await server_auth(ctx, args['password'])
|
||||||
|
|
||||||
if cmd == 'ConnectionRefused':
|
if cmd == 'ConnectionRefused':
|
||||||
password_requested = False
|
|
||||||
if 'InvalidPassword' in args:
|
if 'InvalidPassword' in args:
|
||||||
print('Invalid password')
|
logging.error('Invalid password')
|
||||||
ctx.password = None
|
ctx.password = None
|
||||||
password_requested = True
|
await server_auth(ctx, True)
|
||||||
if 'InvalidName' in args:
|
if 'InvalidRom' in args:
|
||||||
print('Invalid name')
|
raise Exception('Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes')
|
||||||
ctx.name = None
|
|
||||||
if 'NameAlreadyTaken' in args:
|
|
||||||
print('Name already taken')
|
|
||||||
ctx.name = None
|
|
||||||
if 'InvalidTeam' in args:
|
|
||||||
print('Invalid team name')
|
|
||||||
ctx.team = None
|
|
||||||
if 'InvalidSlot' in args:
|
|
||||||
print('Invalid player slot')
|
|
||||||
ctx.slot = None
|
|
||||||
if 'SlotAlreadyTaken' in args:
|
if 'SlotAlreadyTaken' in args:
|
||||||
print('Player slot already in use for that team')
|
raise Exception('Player slot already in use for that team')
|
||||||
ctx.team = None
|
raise Exception('Connection refused by the multiworld host')
|
||||||
ctx.slot = None
|
|
||||||
await server_auth(ctx, password_requested)
|
|
||||||
|
|
||||||
if cmd == 'Connected':
|
if cmd == 'Connected':
|
||||||
ctx.expected_rom = args
|
ctx.team, ctx.slot = args[0]
|
||||||
if ctx.last_rom is not None:
|
ctx.player_names = {p: n for p, n in args[1]}
|
||||||
if ctx.last_rom[:len(args)] == ctx.expected_rom:
|
msgs = []
|
||||||
rom_confirmed(ctx)
|
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
await send_msgs(ctx.socket, [['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]]])
|
msgs.append(['LocationChecks', [Regions.location_table[loc][0] for loc in ctx.locations_checked]])
|
||||||
else:
|
if ctx.locations_scouted:
|
||||||
raise Exception('Different ROM expected from server')
|
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
||||||
|
if msgs:
|
||||||
|
await send_msgs(ctx.socket, msgs)
|
||||||
|
|
||||||
if cmd == 'ReceivedItems':
|
if cmd == 'ReceivedItems':
|
||||||
start_index, items = args
|
start_index, items = args
|
||||||
@@ -640,39 +647,54 @@ async def process_server_cmd(ctx : Context, cmd, args):
|
|||||||
await send_msgs(ctx.socket, sync_msg)
|
await send_msgs(ctx.socket, sync_msg)
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
for item in items:
|
for item in items:
|
||||||
ctx.items_received.append(ReceivedItem(item[0], item[1], item[2], item[3]))
|
ctx.items_received.append(ReceivedItem(*item))
|
||||||
|
ctx.watcher_event.set()
|
||||||
|
|
||||||
|
if cmd == 'LocationInfo':
|
||||||
|
for location, item, player in args:
|
||||||
|
if location not in ctx.locations_info:
|
||||||
|
replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'}
|
||||||
|
item_name = replacements.get(item, get_item_name_from_id(item))
|
||||||
|
logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}")
|
||||||
|
ctx.locations_info[location] = (item, player)
|
||||||
|
ctx.watcher_event.set()
|
||||||
|
|
||||||
if cmd == 'ItemSent':
|
if cmd == 'ItemSent':
|
||||||
player_sent, player_recvd, item, location = args
|
player_sent, location, player_recvd, item = args
|
||||||
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.name else 'green')
|
item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green')
|
||||||
player_sent = color(player_sent, 'yellow' if player_sent != ctx.name else 'magenta')
|
player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta')
|
||||||
player_recvd = color(player_recvd, 'yellow' if player_recvd != ctx.name else 'magenta')
|
player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta')
|
||||||
print('(%s) %s sent %s to %s (%s)' % (ctx.team if ctx.team else 'Team', player_sent, item, player_recvd, get_location_name_from_address(location)))
|
logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location)))
|
||||||
|
|
||||||
if cmd == 'Print':
|
if cmd == 'Print':
|
||||||
print(args)
|
logging.info(args)
|
||||||
|
|
||||||
async def server_auth(ctx : Context, password_requested):
|
async def server_auth(ctx : Context, password_requested):
|
||||||
if password_requested and not ctx.password:
|
if password_requested and not ctx.password:
|
||||||
print('Enter the password required to join this game:')
|
logging.info('Enter the password required to join this game:')
|
||||||
ctx.password = await console_input(ctx)
|
ctx.password = await console_input(ctx)
|
||||||
while not ctx.name or not re.match(r'\w{1,10}', ctx.name):
|
if ctx.rom is None:
|
||||||
print('Enter your name (10 characters):')
|
ctx.awaiting_rom = True
|
||||||
ctx.name = await console_input(ctx)
|
logging.info('No ROM detected, awaiting snes connection to authenticate to the multiworld server')
|
||||||
if not ctx.team:
|
return
|
||||||
print('Enter your team name (optional):')
|
ctx.awaiting_rom = False
|
||||||
ctx.team = await console_input(ctx)
|
ctx.auth = ctx.rom.copy()
|
||||||
if ctx.team == '': ctx.team = None
|
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'rom': ctx.auth}]])
|
||||||
if not ctx.slot:
|
|
||||||
print('Choose your player slot (optional):')
|
|
||||||
slot = await console_input(ctx)
|
|
||||||
ctx.slot = int(slot) if slot.isdigit() else None
|
|
||||||
await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'name': ctx.name, 'team': ctx.team, 'slot': ctx.slot}]])
|
|
||||||
|
|
||||||
async def console_input(ctx : Context):
|
async def console_input(ctx : Context):
|
||||||
ctx.input_requests += 1
|
ctx.input_requests += 1
|
||||||
return await ctx.input_queue.get()
|
return await ctx.input_queue.get()
|
||||||
|
|
||||||
|
async def disconnect(ctx: Context):
|
||||||
|
if ctx.socket is not None and not ctx.socket.closed:
|
||||||
|
await ctx.socket.close()
|
||||||
|
if ctx.server_task is not None:
|
||||||
|
await ctx.server_task
|
||||||
|
|
||||||
|
async def connect(ctx: Context, address=None):
|
||||||
|
await disconnect(ctx)
|
||||||
|
ctx.server_task = asyncio.create_task(server_loop(ctx, address))
|
||||||
|
|
||||||
async def console_loop(ctx : Context):
|
async def console_loop(ctx : Context):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
input = await aioconsole.ainput()
|
input = await aioconsole.ainput()
|
||||||
@@ -682,70 +704,53 @@ async def console_loop(ctx : Context):
|
|||||||
ctx.input_queue.put_nowait(input)
|
ctx.input_queue.put_nowait(input)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
command = input.split()
|
command = shlex.split(input)
|
||||||
if not command:
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if command[0] == '/exit':
|
if command[0] == '/exit':
|
||||||
ctx.exit_event.set()
|
ctx.exit_event.set()
|
||||||
|
|
||||||
if command[0] == '/installcolors' and 'colorama' not in sys.modules:
|
|
||||||
subprocess.call([sys.executable, '-m', 'pip', 'install', '--upgrade', 'colorama'])
|
|
||||||
global colorama
|
|
||||||
import colorama
|
|
||||||
colorama.init()
|
|
||||||
|
|
||||||
if command[0] == '/snes':
|
if command[0] == '/snes':
|
||||||
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else None))
|
ctx.snes_reconnect_address = None
|
||||||
|
asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address))
|
||||||
if command[0] in ['/snes_close', '/snes_quit']:
|
if command[0] in ['/snes_close', '/snes_quit']:
|
||||||
|
ctx.snes_reconnect_address = None
|
||||||
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
|
||||||
await ctx.snes_socket.close()
|
await ctx.snes_socket.close()
|
||||||
|
|
||||||
async def disconnect():
|
|
||||||
if ctx.socket is not None and not ctx.socket.closed:
|
|
||||||
await ctx.socket.close()
|
|
||||||
if ctx.server_task is not None:
|
|
||||||
await ctx.server_task
|
|
||||||
async def connect():
|
|
||||||
await disconnect()
|
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
|
||||||
|
|
||||||
if command[0] in ['/connect', '/reconnect']:
|
if command[0] in ['/connect', '/reconnect']:
|
||||||
if len(command) > 1:
|
ctx.server_address = None
|
||||||
ctx.server_address = command[1]
|
asyncio.create_task(connect(ctx, command[1] if len(command) > 1 else None))
|
||||||
asyncio.create_task(connect())
|
|
||||||
if command[0] == '/disconnect':
|
if command[0] == '/disconnect':
|
||||||
asyncio.create_task(disconnect())
|
ctx.server_address = None
|
||||||
|
asyncio.create_task(disconnect(ctx))
|
||||||
if command[0][:1] != '/':
|
if command[0][:1] != '/':
|
||||||
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
|
asyncio.create_task(send_msgs(ctx.socket, [['Say', input]]))
|
||||||
|
|
||||||
if command[0] == '/received':
|
if command[0] == '/received':
|
||||||
print('Received items:')
|
logging.info('Received items:')
|
||||||
for index, item in enumerate(ctx.items_received, 1):
|
for index, item in enumerate(ctx.items_received, 1):
|
||||||
print('%s from %s (%s) (%d/%d in list)' % (
|
logging.info('%s from %s (%s) (%d/%d in list)' % (
|
||||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
|
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
get_location_name_from_address(item.location), index, len(ctx.items_received)))
|
get_location_name_from_address(item.location), index, len(ctx.items_received)))
|
||||||
|
|
||||||
if command[0] == '/missing':
|
if command[0] == '/missing':
|
||||||
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
|
for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]:
|
||||||
if location not in ctx.locations_checked:
|
if location not in ctx.locations_checked:
|
||||||
print('Missing: ' + location)
|
logging.info('Missing: ' + location)
|
||||||
if command[0] == '/getitem' and len(command) > 1:
|
if command[0] == '/getitem' and len(command) > 1:
|
||||||
item = input[9:]
|
item = input[9:]
|
||||||
item_id = Items.item_table[item][3] if item in Items.item_table else None
|
item_id = Items.item_table[item][3] if item in Items.item_table else None
|
||||||
if type(item_id) is int and item_id in range(0x100):
|
if type(item_id) is int and item_id in range(0x100):
|
||||||
print('Sending item: ' + item)
|
logging.info('Sending item: ' + item)
|
||||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
|
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item_id]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0]))
|
||||||
else:
|
else:
|
||||||
print('Invalid item: ' + item)
|
logging.info('Invalid item: ' + item)
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
await snes_flush_writes(ctx)
|
||||||
|
|
||||||
def rom_confirmed(ctx : Context):
|
|
||||||
ctx.rom_confirmed = True
|
|
||||||
print('ROM hash Confirmed')
|
|
||||||
|
|
||||||
def get_item_name_from_id(code):
|
def get_item_name_from_id(code):
|
||||||
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
|
items = [k for k, i in Items.item_table.items() if type(i[3]) is int and i[3] == code]
|
||||||
return items[0] if items else 'Unknown item'
|
return items[0] if items else 'Unknown item'
|
||||||
@@ -761,7 +766,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||||||
new_locations = []
|
new_locations = []
|
||||||
def new_check(location):
|
def new_check(location):
|
||||||
ctx.locations_checked.add(location)
|
ctx.locations_checked.add(location)
|
||||||
print("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
|
logging.info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
|
||||||
new_locations.append(Regions.location_table[location][0])
|
new_locations.append(Regions.location_table[location][0])
|
||||||
|
|
||||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||||
@@ -820,68 +825,82 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||||||
|
|
||||||
async def game_watcher(ctx : Context):
|
async def game_watcher(ctx : Context):
|
||||||
while not ctx.exit_event.is_set():
|
while not ctx.exit_event.is_set():
|
||||||
await asyncio.sleep(2)
|
try:
|
||||||
|
await asyncio.wait_for(ctx.watcher_event.wait(), 2)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
ctx.watcher_event.clear()
|
||||||
|
|
||||||
if not ctx.rom_confirmed:
|
if not ctx.rom:
|
||||||
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||||
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
|
||||||
continue
|
continue
|
||||||
if list(rom) != ctx.last_rom:
|
|
||||||
ctx.last_rom = list(rom)
|
ctx.rom = list(rom)
|
||||||
ctx.locations_checked = set()
|
ctx.locations_checked = set()
|
||||||
if ctx.expected_rom is not None:
|
ctx.locations_scouted = set()
|
||||||
if ctx.last_rom[:len(ctx.expected_rom)] != ctx.expected_rom:
|
if ctx.awaiting_rom:
|
||||||
print("Wrong ROM detected")
|
await server_auth(ctx, False)
|
||||||
await ctx.snes_socket.close()
|
|
||||||
continue
|
if ctx.auth and ctx.auth != ctx.rom:
|
||||||
else:
|
logging.warning("ROM change detected, please reconnect to the multiworld server")
|
||||||
rom_confirmed(ctx)
|
await disconnect(ctx)
|
||||||
|
|
||||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||||
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
if gamemode is None or gamemode[0] not in INGAME_MODES:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 7)
|
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||||
if data is None:
|
if data is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
recv_index = data[0] | (data[1] << 8)
|
recv_index = data[0] | (data[1] << 8)
|
||||||
assert(RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2)
|
assert RECV_ITEM_ADDR == RECV_PROGRESS_ADDR + 2
|
||||||
recv_item = data[2]
|
recv_item = data[2]
|
||||||
assert(ROOMID_ADDR == RECV_PROGRESS_ADDR + 4)
|
assert ROOMID_ADDR == RECV_PROGRESS_ADDR + 4
|
||||||
roomid = data[4] | (data[5] << 8)
|
roomid = data[4] | (data[5] << 8)
|
||||||
assert(ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6)
|
assert ROOMDATA_ADDR == RECV_PROGRESS_ADDR + 6
|
||||||
roomdata = data[6]
|
roomdata = data[6]
|
||||||
|
assert SCOUT_LOCATION_ADDR == RECV_PROGRESS_ADDR + 7
|
||||||
await track_locations(ctx, roomid, roomdata)
|
scout_location = data[7]
|
||||||
|
|
||||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||||
item = ctx.items_received[recv_index]
|
item = ctx.items_received[recv_index]
|
||||||
print('Received %s from %s (%s) (%d/%d in list)' % (
|
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||||
color(get_item_name_from_id(item.item), 'red', 'bold'), color(item.player_name, 'yellow'),
|
color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
|
||||||
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
|
get_location_name_from_address(item.location), recv_index + 1, len(ctx.items_received)))
|
||||||
recv_index += 1
|
recv_index += 1
|
||||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
|
||||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player_id]))
|
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
|
||||||
|
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||||
|
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
|
||||||
|
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
|
||||||
|
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
|
||||||
|
|
||||||
await snes_flush_writes(ctx)
|
await snes_flush_writes(ctx)
|
||||||
|
|
||||||
|
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||||
|
ctx.locations_scouted.add(scout_location)
|
||||||
|
logging.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
||||||
|
await send_msgs(ctx.socket, [['LocationScouts', [scout_location]]])
|
||||||
|
await track_locations(ctx, roomid, roomdata)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
parser.add_argument('--snes', default='localhost:8080', help='Address of the QUsb2snes server.')
|
||||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||||
parser.add_argument('--name', default=None)
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
parser.add_argument('--team', default=None)
|
|
||||||
parser.add_argument('--slot', default=None, type=int)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
ctx = Context(args.snes, args.connect, args.password, args.name, args.team, args.slot)
|
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||||
|
|
||||||
|
ctx = Context(args.snes, args.connect, args.password)
|
||||||
|
|
||||||
input_task = asyncio.create_task(console_loop(ctx))
|
input_task = asyncio.create_task(console_loop(ctx))
|
||||||
|
|
||||||
await snes_connect(ctx)
|
await snes_connect(ctx, ctx.snes_address)
|
||||||
|
|
||||||
if ctx.server_task is None:
|
if ctx.server_task is None:
|
||||||
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
ctx.server_task = asyncio.create_task(server_loop(ctx))
|
||||||
@@ -890,7 +909,8 @@ async def main():
|
|||||||
|
|
||||||
|
|
||||||
await ctx.exit_event.wait()
|
await ctx.exit_event.wait()
|
||||||
|
ctx.server_address = None
|
||||||
|
ctx.snes_reconnect_address = None
|
||||||
|
|
||||||
await watcher_task
|
await watcher_task
|
||||||
|
|
||||||
@@ -909,13 +929,9 @@ async def main():
|
|||||||
await input_task
|
await input_task
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if 'colorama' in sys.modules:
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
|
loop.run_until_complete(asyncio.gather(*asyncio.Task.all_tasks()))
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
if 'colorama' in sys.modules:
|
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
+128
-119
@@ -5,6 +5,7 @@ import functools
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import websockets
|
import websockets
|
||||||
import zlib
|
import zlib
|
||||||
@@ -27,29 +28,18 @@ class Context:
|
|||||||
self.data_filename = None
|
self.data_filename = None
|
||||||
self.save_filename = None
|
self.save_filename = None
|
||||||
self.disable_save = False
|
self.disable_save = False
|
||||||
self.players = 0
|
self.player_names = {}
|
||||||
self.rom_names = {}
|
self.rom_names = {}
|
||||||
|
self.remote_items = set()
|
||||||
self.locations = {}
|
self.locations = {}
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.password = password
|
self.password = password
|
||||||
self.server = None
|
self.server = None
|
||||||
|
self.countdown_timer = 0
|
||||||
self.clients = []
|
self.clients = []
|
||||||
self.received_items = {}
|
self.received_items = {}
|
||||||
|
|
||||||
def get_room_info(ctx : Context):
|
|
||||||
return {
|
|
||||||
'password': ctx.password is not None,
|
|
||||||
'slots': ctx.players,
|
|
||||||
'players': [(client.name, client.team, client.slot) for client in ctx.clients if client.auth]
|
|
||||||
}
|
|
||||||
|
|
||||||
def same_name(lhs, rhs):
|
|
||||||
return lhs.lower() == rhs.lower()
|
|
||||||
|
|
||||||
def same_team(lhs, rhs):
|
|
||||||
return (type(lhs) is type(rhs)) and ((not lhs and not rhs) or (lhs.lower() == rhs.lower()))
|
|
||||||
|
|
||||||
async def send_msgs(websocket, msgs):
|
async def send_msgs(websocket, msgs):
|
||||||
if not websocket or not websocket.open or websocket.closed:
|
if not websocket or not websocket.open or websocket.closed:
|
||||||
return
|
return
|
||||||
@@ -65,21 +55,21 @@ def broadcast_all(ctx : Context, msgs):
|
|||||||
|
|
||||||
def broadcast_team(ctx : Context, team, msgs):
|
def broadcast_team(ctx : Context, team, msgs):
|
||||||
for client in ctx.clients:
|
for client in ctx.clients:
|
||||||
if client.auth and same_team(client.team, team):
|
if client.auth and client.team == team:
|
||||||
asyncio.create_task(send_msgs(client.socket, msgs))
|
asyncio.create_task(send_msgs(client.socket, msgs))
|
||||||
|
|
||||||
def notify_all(ctx : Context, text):
|
def notify_all(ctx : Context, text):
|
||||||
print("Notice (all): %s" % text)
|
logging.info("Notice (all): %s" % text)
|
||||||
broadcast_all(ctx, [['Print', text]])
|
broadcast_all(ctx, [['Print', text]])
|
||||||
|
|
||||||
def notify_team(ctx : Context, team : str, text : str):
|
def notify_team(ctx : Context, team : int, text : str):
|
||||||
print("Team notice (%s): %s" % ("Default" if not team else team, text))
|
logging.info("Notice (Team #%d): %s" % (team+1, text))
|
||||||
broadcast_team(ctx, team, [['Print', text]])
|
broadcast_team(ctx, team, [['Print', text]])
|
||||||
|
|
||||||
def notify_client(client : Client, text : str):
|
def notify_client(client : Client, text : str):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
print("Player notice (%s): %s" % (client.name, text))
|
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text))
|
||||||
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
|
asyncio.create_task(send_msgs(client.socket, [['Print', text]]))
|
||||||
|
|
||||||
async def server(websocket, path, ctx : Context):
|
async def server(websocket, path, ctx : Context):
|
||||||
@@ -105,54 +95,54 @@ async def server(websocket, path, ctx : Context):
|
|||||||
ctx.clients.remove(client)
|
ctx.clients.remove(client)
|
||||||
|
|
||||||
async def on_client_connected(ctx : Context, client : Client):
|
async def on_client_connected(ctx : Context, client : Client):
|
||||||
await send_msgs(client.socket, [['RoomInfo', get_room_info(ctx)]])
|
await send_msgs(client.socket, [['RoomInfo', {
|
||||||
|
'password': ctx.password is not None,
|
||||||
|
'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth]
|
||||||
|
}]])
|
||||||
|
|
||||||
async def on_client_disconnected(ctx : Context, client : Client):
|
async def on_client_disconnected(ctx : Context, client : Client):
|
||||||
if client.auth:
|
if client.auth:
|
||||||
await on_client_left(ctx, client)
|
await on_client_left(ctx, client)
|
||||||
|
|
||||||
async def on_client_joined(ctx : Context, client : Client):
|
async def on_client_joined(ctx : Context, client : Client):
|
||||||
notify_all(ctx, "%s has joined the game as player %d for %s" % (client.name, client.slot, "the default team" if not client.team else "team %s" % client.team))
|
notify_all(ctx, "%s (Team #%d) has joined the game" % (client.name, client.team + 1))
|
||||||
|
|
||||||
async def on_client_left(ctx : Context, client : Client):
|
async def on_client_left(ctx : Context, client : Client):
|
||||||
notify_all(ctx, "%s (Player %d, %s) has left the game" % (client.name, client.slot, "Default team" if not client.team else "Team %s" % client.team))
|
notify_all(ctx, "%s (Team #%d) has left the game" % (client.name, client.team + 1))
|
||||||
|
|
||||||
|
async def countdown(ctx : Context, timer):
|
||||||
|
notify_all(ctx, f'[Server]: Starting countdown of {timer}s')
|
||||||
|
if ctx.countdown_timer:
|
||||||
|
ctx.countdown_timer = timer
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx.countdown_timer = timer
|
||||||
|
while ctx.countdown_timer > 0:
|
||||||
|
notify_all(ctx, f'[Server]: {ctx.countdown_timer}')
|
||||||
|
ctx.countdown_timer -= 1
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
notify_all(ctx, f'[Server]: GO')
|
||||||
|
|
||||||
def get_connected_players_string(ctx : Context):
|
def get_connected_players_string(ctx : Context):
|
||||||
auth_clients = [c for c in ctx.clients if c.auth]
|
auth_clients = [c for c in ctx.clients if c.auth]
|
||||||
if not auth_clients:
|
if not auth_clients:
|
||||||
return 'No player connected'
|
return 'No player connected'
|
||||||
|
|
||||||
auth_clients.sort(key=lambda c: ('' if not c.team else c.team.lower(), c.slot))
|
auth_clients.sort(key=lambda c: (c.team, c.slot))
|
||||||
current_team = 0
|
current_team = 0
|
||||||
text = ''
|
text = 'Team #1: '
|
||||||
for c in auth_clients:
|
for c in auth_clients:
|
||||||
if c.team != current_team:
|
if c.team != current_team:
|
||||||
text += '::' + ('default team' if not c.team else c.team) + ':: '
|
text += f':: Team #{c.team + 1}: '
|
||||||
current_team = c.team
|
current_team = c.team
|
||||||
text += '%d:%s ' % (c.slot, c.name)
|
text += f'{c.name} '
|
||||||
return 'Connected players: ' + text[:-1]
|
return 'Connected players: ' + text[:-1]
|
||||||
|
|
||||||
def get_player_name_in_team(ctx : Context, team, slot):
|
|
||||||
for client in ctx.clients:
|
|
||||||
if client.auth and same_team(team, client.team) and client.slot == slot:
|
|
||||||
return client.name
|
|
||||||
return "Player %d" % slot
|
|
||||||
|
|
||||||
def get_client_from_name(ctx : Context, name):
|
|
||||||
for client in ctx.clients:
|
|
||||||
if client.auth and same_name(name, client.name):
|
|
||||||
return client
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_received_items(ctx : Context, team, player):
|
def get_received_items(ctx : Context, team, player):
|
||||||
for (c_team, c_id), items in ctx.received_items.items():
|
return ctx.received_items.setdefault((team, player), [])
|
||||||
if c_id == player and same_team(c_team, team):
|
|
||||||
return items
|
|
||||||
ctx.received_items[(team, player)] = []
|
|
||||||
return ctx.received_items[(team, player)]
|
|
||||||
|
|
||||||
def tuplize_received_items(items):
|
def tuplize_received_items(items):
|
||||||
return [(item.item, item.location, item.player_id, item.player_name) for item in items]
|
return [(item.item, item.location, item.player) for item in items]
|
||||||
|
|
||||||
def send_new_items(ctx : Context):
|
def send_new_items(ctx : Context):
|
||||||
for client in ctx.clients:
|
for client in ctx.clients:
|
||||||
@@ -163,37 +153,36 @@ def send_new_items(ctx : Context):
|
|||||||
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
|
asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]]))
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
def forfeit_player(ctx : Context, team, slot, name):
|
def forfeit_player(ctx : Context, team, slot):
|
||||||
all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int]
|
all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int]
|
||||||
notify_all(ctx, "%s (Player %d) in team %s has forfeited" % (name, slot, team if team else 'default'))
|
notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
|
||||||
register_location_checks(ctx, name, team, slot, all_locations)
|
register_location_checks(ctx, team, slot, all_locations)
|
||||||
|
|
||||||
def register_location_checks(ctx : Context, name, team, slot, locations):
|
def register_location_checks(ctx : Context, team, slot, locations):
|
||||||
found_items = False
|
found_items = False
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if (location, slot) in ctx.locations:
|
if (location, slot) in ctx.locations:
|
||||||
target_item, target_player = ctx.locations[(location, slot)]
|
target_item, target_player = ctx.locations[(location, slot)]
|
||||||
if target_player != slot:
|
if target_player != slot or slot in ctx.remote_items:
|
||||||
found = False
|
found = False
|
||||||
recvd_items = get_received_items(ctx, team, target_player)
|
recvd_items = get_received_items(ctx, team, target_player)
|
||||||
for recvd_item in recvd_items:
|
for recvd_item in recvd_items:
|
||||||
if recvd_item.location == location and recvd_item.player_id == slot:
|
if recvd_item.location == location and recvd_item.player == slot:
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
new_item = ReceivedItem(target_item, location, slot, name)
|
new_item = ReceivedItem(target_item, location, slot)
|
||||||
recvd_items.append(new_item)
|
recvd_items.append(new_item)
|
||||||
target_player_name = get_player_name_in_team(ctx, team, target_player)
|
if slot != target_player:
|
||||||
broadcast_team(ctx, team, [['ItemSent', (name, target_player_name, target_item, location)]])
|
broadcast_team(ctx, team, [['ItemSent', (slot, location, target_player, target_item)]])
|
||||||
print('(%s) %s sent %s to %s (%s)' % (team if team else 'Team', name, get_item_name_from_id(target_item), target_player_name, get_location_name_from_address(location)))
|
logging.info('(Team #%d) %s sent %s to %s (%s)' % (team+1, ctx.player_names[(team, slot)], get_item_name_from_id(target_item), ctx.player_names[(team, target_player)], get_location_name_from_address(location)))
|
||||||
found_items = True
|
found_items = True
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
|
|
||||||
if found_items and not ctx.disable_save:
|
if found_items and not ctx.disable_save:
|
||||||
try:
|
try:
|
||||||
with open(ctx.save_filename, "wb") as f:
|
with open(ctx.save_filename, "wb") as f:
|
||||||
jsonstr = json.dumps((ctx.players,
|
jsonstr = json.dumps((list(ctx.rom_names.items()),
|
||||||
[(k, v) for k, v in ctx.rom_names.items()],
|
|
||||||
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
|
[(k, [i.__dict__ for i in v]) for k, v in ctx.received_items.items()]))
|
||||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -207,50 +196,30 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||||||
if cmd == 'Connect':
|
if cmd == 'Connect':
|
||||||
if not args or type(args) is not dict or \
|
if not args or type(args) is not dict or \
|
||||||
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||||
'name' not in args or type(args['name']) is not str or \
|
'rom' not in args or type(args['rom']) is not list:
|
||||||
'team' not in args or type(args['team']) not in [str, type(None)] or \
|
|
||||||
'slot' not in args or type(args['slot']) not in [int, type(None)]:
|
|
||||||
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
|
await send_msgs(client.socket, [['InvalidArguments', 'Connect']])
|
||||||
return
|
return
|
||||||
|
|
||||||
errors = set()
|
errors = set()
|
||||||
if ctx.password is not None and ('password' not in args or args['password'] != ctx.password):
|
if ctx.password is not None and args['password'] != ctx.password:
|
||||||
errors.add('InvalidPassword')
|
errors.add('InvalidPassword')
|
||||||
|
|
||||||
if 'name' not in args or not args['name'] or not re.match(r'\w{1,10}', args['name']):
|
if tuple(args['rom']) not in ctx.rom_names:
|
||||||
errors.add('InvalidName')
|
errors.add('InvalidRom')
|
||||||
elif any([same_name(c.name, args['name']) for c in ctx.clients if c.auth]):
|
|
||||||
errors.add('NameAlreadyTaken')
|
|
||||||
else:
|
else:
|
||||||
client.name = args['name']
|
team, slot = ctx.rom_names[tuple(args['rom'])]
|
||||||
|
if any([c.slot == slot and c.team == team for c in ctx.clients if c.auth]):
|
||||||
if 'team' in args and args['team'] is not None and not re.match(r'\w{1,15}', args['team']):
|
|
||||||
errors.add('InvalidTeam')
|
|
||||||
else:
|
|
||||||
client.team = args['team'] if 'team' in args else None
|
|
||||||
|
|
||||||
if 'slot' in args and any([c.slot == args['slot'] for c in ctx.clients if c.auth and same_team(c.team, client.team)]):
|
|
||||||
errors.add('SlotAlreadyTaken')
|
errors.add('SlotAlreadyTaken')
|
||||||
elif 'slot' not in args or not args['slot']:
|
else:
|
||||||
for slot in range(1, ctx.players + 1):
|
client.name = ctx.player_names[(team, slot)]
|
||||||
if slot not in [c.slot for c in ctx.clients if c.auth and same_team(c.team, client.team)]:
|
client.team = team
|
||||||
client.slot = slot
|
client.slot = slot
|
||||||
break
|
|
||||||
elif slot == ctx.players:
|
|
||||||
errors.add('SlotAlreadyTaken')
|
|
||||||
elif args['slot'] not in range(1, ctx.players + 1):
|
|
||||||
errors.add('InvalidSlot')
|
|
||||||
else:
|
|
||||||
client.slot = args['slot']
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
client.name = None
|
|
||||||
client.team = None
|
|
||||||
client.slot = None
|
|
||||||
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
|
await send_msgs(client.socket, [['ConnectionRefused', list(errors)]])
|
||||||
else:
|
else:
|
||||||
client.auth = True
|
client.auth = True
|
||||||
reply = [['Connected', ctx.rom_names[client.slot]]]
|
reply = [['Connected', [(client.team, client.slot), [(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]]
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
|
reply.append(['ReceivedItems', (0, tuplize_received_items(items))])
|
||||||
@@ -265,13 +234,35 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
await send_msgs(client.socket, ['ReceivedItems', (0, tuplize_received_items(items))])
|
await send_msgs(client.socket, [['ReceivedItems', (0, tuplize_received_items(items))]])
|
||||||
|
|
||||||
if cmd == 'LocationChecks':
|
if cmd == 'LocationChecks':
|
||||||
if type(args) is not list:
|
if type(args) is not list:
|
||||||
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
|
await send_msgs(client.socket, [['InvalidArguments', 'LocationChecks']])
|
||||||
return
|
return
|
||||||
register_location_checks(ctx, client.name, client.team, client.slot, args)
|
register_location_checks(ctx, client.team, client.slot, args)
|
||||||
|
|
||||||
|
if cmd == 'LocationScouts':
|
||||||
|
if type(args) is not list:
|
||||||
|
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||||
|
return
|
||||||
|
locs = []
|
||||||
|
for location in args:
|
||||||
|
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||||
|
await send_msgs(client.socket, [['InvalidArguments', 'LocationScouts']])
|
||||||
|
return
|
||||||
|
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||||
|
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||||
|
|
||||||
|
replacements = {'SmallKey': 0xA2, 'BigKey': 0x9D, 'Compass': 0x8D, 'Map': 0x7D}
|
||||||
|
item_type = [i[2] for i in Items.item_table.values() if type(i[3]) is int and i[3] == target_item]
|
||||||
|
if item_type:
|
||||||
|
target_item = replacements.get(item_type[0], target_item)
|
||||||
|
|
||||||
|
locs.append([loc_name, location, target_item, target_player])
|
||||||
|
|
||||||
|
logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||||
|
await send_msgs(client.socket, [['LocationInfo', [l[1:] for l in locs]]])
|
||||||
|
|
||||||
if cmd == 'Say':
|
if cmd == 'Say':
|
||||||
if type(args) is not str or not args.isprintable():
|
if type(args) is not str or not args.isprintable():
|
||||||
@@ -280,20 +271,26 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
|||||||
|
|
||||||
notify_all(ctx, client.name + ': ' + args)
|
notify_all(ctx, client.name + ': ' + args)
|
||||||
|
|
||||||
if args[:8] == '!players':
|
if args.startswith('!players'):
|
||||||
notify_all(ctx, get_connected_players_string(ctx))
|
notify_all(ctx, get_connected_players_string(ctx))
|
||||||
if args[:8] == '!forfeit':
|
if args.startswith('!forfeit'):
|
||||||
forfeit_player(ctx, client.team, client.slot, client.name)
|
forfeit_player(ctx, client.team, client.slot)
|
||||||
|
if args.startswith('!countdown'):
|
||||||
|
try:
|
||||||
|
timer = int(args.split()[1])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
timer = 10
|
||||||
|
asyncio.create_task(countdown(ctx, timer))
|
||||||
|
|
||||||
def set_password(ctx : Context, password):
|
def set_password(ctx : Context, password):
|
||||||
ctx.password = password
|
ctx.password = password
|
||||||
print('Password set to ' + password if password is not None else 'Password disabled')
|
logging.warning('Password set to ' + password if password is not None else 'Password disabled')
|
||||||
|
|
||||||
async def console(ctx : Context):
|
async def console(ctx : Context):
|
||||||
while True:
|
while True:
|
||||||
input = await aioconsole.ainput()
|
input = await aioconsole.ainput()
|
||||||
|
|
||||||
command = input.split()
|
command = shlex.split(input)
|
||||||
if not command:
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -302,34 +299,41 @@ async def console(ctx : Context):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if command[0] == '/players':
|
if command[0] == '/players':
|
||||||
print(get_connected_players_string(ctx))
|
logging.info(get_connected_players_string(ctx))
|
||||||
if command[0] == '/password':
|
if command[0] == '/password':
|
||||||
set_password(ctx, command[1] if len(command) > 1 else None)
|
set_password(ctx, command[1] if len(command) > 1 else None)
|
||||||
if command[0] == '/kick' and len(command) > 1:
|
if command[0] == '/kick' and len(command) > 1:
|
||||||
client = get_client_from_name(ctx, command[1])
|
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
|
||||||
if client and client.socket and not client.socket.closed:
|
for client in ctx.clients:
|
||||||
|
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
|
||||||
|
if client.socket and not client.socket.closed:
|
||||||
await client.socket.close()
|
await client.socket.close()
|
||||||
|
|
||||||
if command[0] == '/forfeitslot' and len(command) == 3 and command[2].isdigit():
|
if command[0] == '/forfeitslot' and len(command) > 1 and command[1].isdigit():
|
||||||
team = command[1] if command[1] != 'default' else None
|
if len(command) > 2 and command[2].isdigit():
|
||||||
|
team = int(command[1]) - 1
|
||||||
slot = int(command[2])
|
slot = int(command[2])
|
||||||
name = get_player_name_in_team(ctx, team, slot)
|
else:
|
||||||
forfeit_player(ctx, team, slot, name)
|
team = 0
|
||||||
|
slot = int(command[1])
|
||||||
|
forfeit_player(ctx, team, slot)
|
||||||
if command[0] == '/forfeitplayer' and len(command) > 1:
|
if command[0] == '/forfeitplayer' and len(command) > 1:
|
||||||
client = get_client_from_name(ctx, command[1])
|
team = int(command[2]) - 1 if len(command) > 2 and command[2].isdigit() else None
|
||||||
if client:
|
for client in ctx.clients:
|
||||||
forfeit_player(ctx, client.team, client.slot, client.name)
|
if client.auth and client.name.lower() == command[1].lower() and (team is None or team == client.team):
|
||||||
|
if client.socket and not client.socket.closed:
|
||||||
|
forfeit_player(ctx, client.team, client.slot)
|
||||||
if command[0] == '/senditem' and len(command) > 2:
|
if command[0] == '/senditem' and len(command) > 2:
|
||||||
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
|
[(player, item)] = re.findall(r'\S* (\S*) (.*)', input)
|
||||||
if item in Items.item_table:
|
if item in Items.item_table:
|
||||||
client = get_client_from_name(ctx, player)
|
for client in ctx.clients:
|
||||||
if client:
|
if client.auth and client.name.lower() == player.lower():
|
||||||
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", 0, "server")
|
new_item = ReceivedItem(Items.item_table[item][3], "cheat console", client.slot)
|
||||||
get_received_items(ctx, client.team, client.slot).append(new_item)
|
get_received_items(ctx, client.team, client.slot).append(new_item)
|
||||||
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
|
notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name)
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
else:
|
else:
|
||||||
print("Unknown item: " + item)
|
logging.warning("Unknown item: " + item)
|
||||||
|
|
||||||
if command[0][0] != '/':
|
if command[0][0] != '/':
|
||||||
notify_all(ctx, '[Server]: ' + input)
|
notify_all(ctx, '[Server]: ' + input)
|
||||||
@@ -342,8 +346,11 @@ async def main():
|
|||||||
parser.add_argument('--multidata', default=None)
|
parser.add_argument('--multidata', default=None)
|
||||||
parser.add_argument('--savefile', default=None)
|
parser.add_argument('--savefile', default=None)
|
||||||
parser.add_argument('--disable_save', default=False, action='store_true')
|
parser.add_argument('--disable_save', default=False, action='store_true')
|
||||||
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||||
|
|
||||||
ctx = Context(args.host, args.port, args.password)
|
ctx = Context(args.host, args.port, args.password)
|
||||||
|
|
||||||
ctx.data_filename = args.multidata
|
ctx.data_filename = args.multidata
|
||||||
@@ -358,15 +365,18 @@ async def main():
|
|||||||
|
|
||||||
with open(ctx.data_filename, 'rb') as f:
|
with open(ctx.data_filename, 'rb') as f:
|
||||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||||
ctx.players = jsonobj[0]
|
for team, names in enumerate(jsonobj['names']):
|
||||||
ctx.rom_names = {k: v for k, v in jsonobj[1]}
|
for player, name in enumerate(names, 1):
|
||||||
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj[2]}
|
ctx.player_names[(team, player)] = name
|
||||||
|
ctx.rom_names = {tuple(rom): (team, slot) for slot, team, rom in jsonobj['roms']}
|
||||||
|
ctx.remote_items = set(jsonobj['remote_items'])
|
||||||
|
ctx.locations = {tuple(k): tuple(v) for k, v in jsonobj['locations']}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Failed to read multiworld data (%s)' % e)
|
logging.error('Failed to read multiworld data (%s)' % e)
|
||||||
return
|
return
|
||||||
|
|
||||||
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
|
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') if not ctx.host else ctx.host
|
||||||
print('Hosting game of %d players (%s) at %s:%d' % (ctx.players, 'No password' if not ctx.password else 'Password: %s' % ctx.password, ip, ctx.port))
|
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||||
|
|
||||||
ctx.disable_save = args.disable_save
|
ctx.disable_save = args.disable_save
|
||||||
if not ctx.disable_save:
|
if not ctx.disable_save:
|
||||||
@@ -375,17 +385,16 @@ async def main():
|
|||||||
try:
|
try:
|
||||||
with open(ctx.save_filename, 'rb') as f:
|
with open(ctx.save_filename, 'rb') as f:
|
||||||
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
jsonobj = json.loads(zlib.decompress(f.read()).decode("utf-8"))
|
||||||
players = jsonobj[0]
|
rom_names = jsonobj[0]
|
||||||
rom_names = {k: v for k, v in jsonobj[1]}
|
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[1]}
|
||||||
received_items = {tuple(k): [ReceivedItem(**i) for i in v] for k, v in jsonobj[2]}
|
if not all([ctx.rom_names[tuple(rom)] == (team, slot) for rom, (team, slot) in rom_names]):
|
||||||
if players != ctx.players or rom_names != ctx.rom_names:
|
|
||||||
raise Exception('Save file mismatch, will start a new game')
|
raise Exception('Save file mismatch, will start a new game')
|
||||||
ctx.received_items = received_items
|
ctx.received_items = received_items
|
||||||
print('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
|
logging.info('Loaded save file with %d received items for %d players' % (sum([len(p) for p in received_items.values()]), len(received_items)))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print('No save data found, starting a new game')
|
logging.error('No save data found, starting a new game')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logging.info(e)
|
||||||
|
|
||||||
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
|
ctx.server = websockets.serve(functools.partial(server,ctx=ctx), ctx.host, ctx.port, ping_timeout=None, ping_interval=None)
|
||||||
await ctx.server
|
await ctx.server
|
||||||
|
|||||||
+4
-2
@@ -39,6 +39,7 @@ def main():
|
|||||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||||
parser.add_argument('--names', default='')
|
parser.add_argument('--names', default='')
|
||||||
|
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
|
||||||
parser.add_argument('--create_spoiler', action='store_true')
|
parser.add_argument('--create_spoiler', action='store_true')
|
||||||
parser.add_argument('--rom')
|
parser.add_argument('--rom')
|
||||||
parser.add_argument('--enemizercli')
|
parser.add_argument('--enemizercli')
|
||||||
@@ -146,13 +147,14 @@ def roll_settings(weights):
|
|||||||
door_shuffle = get_choice('door_shuffle')
|
door_shuffle = get_choice('door_shuffle')
|
||||||
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
||||||
|
|
||||||
|
goal = get_choice('goals')
|
||||||
ret.goal = {'ganon': 'ganon',
|
ret.goal = {'ganon': 'ganon',
|
||||||
'fast_ganon': 'crystals',
|
'fast_ganon': 'crystals',
|
||||||
'dungeons': 'dungeons',
|
'dungeons': 'dungeons',
|
||||||
'pedestal': 'pedestal',
|
'pedestal': 'pedestal',
|
||||||
'triforce-hunt': 'triforcehunt'
|
'triforce-hunt': 'triforcehunt'
|
||||||
}[get_choice('goals')]
|
}[goal]
|
||||||
ret.openpyramid = ret.goal == 'fast_ganon'
|
ret.openpyramid = goal == 'fast_ganon'
|
||||||
|
|
||||||
ret.crystals_gt = get_choice('tower_open')
|
ret.crystals_gt = get_choice('tower_open')
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ def main(args):
|
|||||||
|
|
||||||
# initialize the world
|
# initialize the world
|
||||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||||
|
world.player_names[1].append("Player 1")
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
|
|
||||||
hasher = hashlib.md5()
|
hasher = hashlib.md5()
|
||||||
@@ -69,7 +70,7 @@ def main(args):
|
|||||||
logger.info('Patching ROM.')
|
logger.info('Patching ROM.')
|
||||||
|
|
||||||
rom = LocalRom(args.rom)
|
rom = LocalRom(args.rom)
|
||||||
patch_rom(world, 1, rom, False)
|
patch_rom(world, rom, 1, 1, False)
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -3,7 +3,7 @@ from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
|
|||||||
|
|
||||||
|
|
||||||
def create_regions(world, player):
|
def create_regions(world, player):
|
||||||
|
std_flag = world.mode[player] == 'standard'
|
||||||
world.regions += [
|
world.regions += [
|
||||||
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'],
|
create_lw_region(player, 'Light World', ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest'],
|
||||||
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
|
["Blinds Hideout", "Hyrule Castle Secret Entrance Drop", 'Zoras River', 'Kings Grave Outer Rocks', 'Dam',
|
||||||
@@ -219,7 +219,10 @@ def create_regions(world, player):
|
|||||||
create_dungeon_region(player, 'Hyrule Dungeon Armory Boomerang', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Boomerang Guard Key Drop'], ['Hyrule Dungeon Armory Boomerang WS']),
|
create_dungeon_region(player, 'Hyrule Dungeon Armory Boomerang', 'Hyrule Castle', ['Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Boomerang Guard Key Drop'], ['Hyrule Dungeon Armory Boomerang WS']),
|
||||||
create_dungeon_region(player, 'Hyrule Dungeon Armory North Branch', 'Hyrule Castle', None, ['Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Down Stairs']),
|
create_dungeon_region(player, 'Hyrule Dungeon Armory North Branch', 'Hyrule Castle', None, ['Hyrule Dungeon Armory Interior Key Door S', 'Hyrule Dungeon Armory Down Stairs']),
|
||||||
create_dungeon_region(player, 'Hyrule Dungeon Staircase', 'Hyrule Castle', None, ['Hyrule Dungeon Staircase Up Stairs', 'Hyrule Dungeon Staircase Down Stairs']),
|
create_dungeon_region(player, 'Hyrule Dungeon Staircase', 'Hyrule Castle', None, ['Hyrule Dungeon Staircase Up Stairs', 'Hyrule Dungeon Staircase Down Stairs']),
|
||||||
create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle', ['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest'], ['Hyrule Dungeon Cellblock Up Stairs']),
|
create_dungeon_region(player, 'Hyrule Dungeon Cellblock', 'Hyrule Castle',
|
||||||
|
['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest'] if not std_flag else
|
||||||
|
['Hyrule Castle - Big Key Drop', 'Hyrule Castle - Zelda\'s Chest', 'Zelda Pickup'],
|
||||||
|
['Hyrule Dungeon Cellblock Up Stairs']),
|
||||||
|
|
||||||
|
|
||||||
create_dungeon_region(player, 'Sewers Behind Tapestry', 'Hyrule Castle', None, ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs']),
|
create_dungeon_region(player, 'Sewers Behind Tapestry', 'Hyrule Castle', None, ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs']),
|
||||||
@@ -233,7 +236,9 @@ def create_regions(world, player):
|
|||||||
['Sewers Secret Room ES', 'Sewers Secret Room EN']),
|
['Sewers Secret Room ES', 'Sewers Secret Room EN']),
|
||||||
create_dungeon_region(player, 'Sewers Yet More Rats', 'Hyrule Castle', None, ['Sewers Pull Switch Down Stairs', 'Sewers Yet More Rats S']),
|
create_dungeon_region(player, 'Sewers Yet More Rats', 'Hyrule Castle', None, ['Sewers Pull Switch Down Stairs', 'Sewers Yet More Rats S']),
|
||||||
create_dungeon_region(player, 'Sewers Pull Switch', 'Hyrule Castle', None, ['Sewers Pull Switch N', 'Sewers Pull Switch S']),
|
create_dungeon_region(player, 'Sewers Pull Switch', 'Hyrule Castle', None, ['Sewers Pull Switch N', 'Sewers Pull Switch S']),
|
||||||
create_dungeon_region(player, 'Sanctuary', 'Hyrule Castle', ['Sanctuary'], ['Sanctuary Exit', 'Sanctuary N']),
|
create_dungeon_region(player, 'Sanctuary', 'Hyrule Castle',
|
||||||
|
['Sanctuary'] if not std_flag else ['Sanctuary', 'Zelda Drop Off'],
|
||||||
|
['Sanctuary Exit', 'Sanctuary N']),
|
||||||
|
|
||||||
# Eastern Palace
|
# Eastern Palace
|
||||||
create_dungeon_region(player, 'Eastern Lobby', 'Eastern Palace', None, ['Eastern Lobby N', 'Eastern Palace Exit', 'Eastern Lobby NW', 'Eastern Lobby NE']),
|
create_dungeon_region(player, 'Eastern Lobby', 'Eastern Palace', None, ['Eastern Lobby N', 'Eastern Palace Exit', 'Eastern Lobby NW', 'Eastern Lobby NE']),
|
||||||
@@ -841,7 +846,9 @@ dungeon_events = [
|
|||||||
'Attic Cracked Floor',
|
'Attic Cracked Floor',
|
||||||
'Suspicious Maiden',
|
'Suspicious Maiden',
|
||||||
'Revealing Light',
|
'Revealing Light',
|
||||||
'Ice Block Drop'
|
'Ice Block Drop',
|
||||||
|
'Zelda Pickup',
|
||||||
|
'Zelda Drop Off'
|
||||||
]
|
]
|
||||||
|
|
||||||
flooded_keys_reverse = {
|
flooded_keys_reverse = {
|
||||||
@@ -1078,6 +1085,8 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
|||||||
'Suspicious Maiden': (None, None, False, None),
|
'Suspicious Maiden': (None, None, False, None),
|
||||||
'Revealing Light': (None, None, False, None),
|
'Revealing Light': (None, None, False, None),
|
||||||
'Ice Block Drop': (None, None, False, None),
|
'Ice Block Drop': (None, None, False, None),
|
||||||
|
'Zelda Pickup': (None, None, False, None),
|
||||||
|
'Zelda Drop Off': (None, None, False, None),
|
||||||
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
'Eastern Palace - Prize': ([0x1209D, 0x53EF8, 0x53EF9, 0x180052, 0x18007C, 0xC6FE], None, True, 'Eastern Palace'),
|
||||||
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
'Desert Palace - Prize': ([0x1209E, 0x53F1C, 0x53F1D, 0x180053, 0x180078, 0xC6FF], None, True, 'Desert Palace'),
|
||||||
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
'Tower of Hera - Prize': ([0x120A5, 0x53F0A, 0x53F0B, 0x18005A, 0x18007A, 0xC706], None, True, 'Tower of Hera'),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import subprocess
|
|||||||
from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
|
from BaseClasses import CollectionState, ShopType, Region, Location, Item, DoorType
|
||||||
from DoorShuffle import compass_data
|
from DoorShuffle import compass_data
|
||||||
from Dungeons import dungeon_music_addresses
|
from Dungeons import dungeon_music_addresses
|
||||||
|
from Regions import location_table
|
||||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||||
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
|
from Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, Blind_texts, BombShop2_texts, junk_texts
|
||||||
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts, LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||||
@@ -21,13 +22,14 @@ from EntranceShuffle import door_addresses, exit_ids
|
|||||||
|
|
||||||
|
|
||||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||||
RANDOMIZERBASEHASH = 'a5ae693acf449264533429d4dbd217e4'
|
RANDOMIZERBASEHASH = 'c1361fcf13239f8677bacc6f9bc5e9dd'
|
||||||
|
|
||||||
|
|
||||||
class JsonRom(object):
|
class JsonRom(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, name=None, hash=None):
|
||||||
self.name = None
|
self.name = name
|
||||||
|
self.hash = hash
|
||||||
self.orig_buffer = None
|
self.orig_buffer = None
|
||||||
self.patches = {}
|
self.patches = {}
|
||||||
self.addresses = []
|
self.addresses = []
|
||||||
@@ -72,8 +74,9 @@ class JsonRom(object):
|
|||||||
|
|
||||||
class LocalRom(object):
|
class LocalRom(object):
|
||||||
|
|
||||||
def __init__(self, file, patch=True):
|
def __init__(self, file, patch=True, name=None, hash=None):
|
||||||
self.name = None
|
self.name = name
|
||||||
|
self.hash = hash
|
||||||
self.orig_buffer = None
|
self.orig_buffer = None
|
||||||
with open(file, 'rb') as stream:
|
with open(file, 'rb') as stream:
|
||||||
self.buffer = read_rom(stream)
|
self.buffer = read_rom(stream)
|
||||||
@@ -92,6 +95,14 @@ class LocalRom(object):
|
|||||||
with open(file, 'wb') as outfile:
|
with open(file, 'wb') as outfile:
|
||||||
outfile.write(self.buffer)
|
outfile.write(self.buffer)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromJsonRom(rom, file, rom_size = 0x200000):
|
||||||
|
ret = LocalRom(file, True, rom.name, rom.hash)
|
||||||
|
ret.buffer.extend(bytearray([0x00] * (rom_size - len(ret.buffer))))
|
||||||
|
for address, values in rom.patches.items():
|
||||||
|
ret.write_bytes(int(address), values)
|
||||||
|
return ret
|
||||||
|
|
||||||
def patch_base_rom(self):
|
def patch_base_rom(self):
|
||||||
# verify correct checksum of baserom
|
# verify correct checksum of baserom
|
||||||
basemd5 = hashlib.md5()
|
basemd5 = hashlib.md5()
|
||||||
@@ -116,11 +127,6 @@ class LocalRom(object):
|
|||||||
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
if RANDOMIZERBASEHASH != patchedmd5.hexdigest():
|
||||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||||
|
|
||||||
def merge_enemizer_patches(self, patches):
|
|
||||||
self.buffer.extend(bytearray([0x00] * (0x400000 - len(self.buffer))))
|
|
||||||
for address, values in patches.items():
|
|
||||||
self.write_bytes(int(address), values)
|
|
||||||
|
|
||||||
def write_crc(self):
|
def write_crc(self):
|
||||||
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||||
inv = crc ^ 0xFFFF
|
inv = crc ^ 0xFFFF
|
||||||
@@ -471,7 +477,7 @@ class Sprite(object):
|
|||||||
# split into palettes of 15 colors
|
# split into palettes of 15 colors
|
||||||
return array_chunk(palette_as_colors, 15)
|
return array_chunk(palette_as_colors, 15)
|
||||||
|
|
||||||
def patch_rom(world, player, rom, enemized):
|
def patch_rom(world, rom, player, team, enemized):
|
||||||
random.seed(world.rom_seeds[player])
|
random.seed(world.rom_seeds[player])
|
||||||
|
|
||||||
# progressive bow silver arrow hint hack
|
# progressive bow silver arrow hint hack
|
||||||
@@ -492,10 +498,10 @@ def patch_rom(world, player, rom, enemized):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not location.crystal:
|
if not location.crystal:
|
||||||
|
if location.item is not None:
|
||||||
# Keys in their native dungeon should use the orignal item code for keys
|
# Keys in their native dungeon should use the orignal item code for keys
|
||||||
if location.parent_region.dungeon:
|
if location.parent_region.dungeon:
|
||||||
dungeon = location.parent_region.dungeon
|
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
||||||
if location.item is not None and dungeon.is_dungeon_item(location.item):
|
|
||||||
if location.item.bigkey:
|
if location.item.bigkey:
|
||||||
itemid = 0x32
|
itemid = 0x32
|
||||||
if location.item.smallkey:
|
if location.item.smallkey:
|
||||||
@@ -504,7 +510,11 @@ def patch_rom(world, player, rom, enemized):
|
|||||||
itemid = 0x33
|
itemid = 0x33
|
||||||
if location.item.compass:
|
if location.item.compass:
|
||||||
itemid = 0x25
|
itemid = 0x25
|
||||||
if location.item and location.item.player != player:
|
if world.remote_items[player]:
|
||||||
|
itemid = list(location_table.keys()).index(location.name) + 1
|
||||||
|
assert itemid < 0x100
|
||||||
|
rom.write_byte(location.player_address, 0xFF)
|
||||||
|
elif location.item.player != player:
|
||||||
if location.player_address is not None:
|
if location.player_address is not None:
|
||||||
rom.write_byte(location.player_address, location.item.player)
|
rom.write_byte(location.player_address, location.item.player)
|
||||||
else:
|
else:
|
||||||
@@ -1255,16 +1265,22 @@ def patch_rom(world, player, rom, enemized):
|
|||||||
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
|
||||||
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
|
||||||
|
|
||||||
write_strings(rom, world, player)
|
write_strings(rom, world, player, team)
|
||||||
|
|
||||||
|
rom.write_byte(0x18636C, 1 if world.remote_items[player] else 0)
|
||||||
|
|
||||||
# set rom name
|
# set rom name
|
||||||
# 21 bytes
|
# 21 bytes
|
||||||
from Main import __version__
|
from Main import __version__
|
||||||
# todo: change to DR when Enemizer is okay with DR
|
# todo: change to DR when Enemizer is okay with DR
|
||||||
rom.name = bytearray('ER_{0}_{1:09}\0'.format(__version__[0:7], world.seed), 'utf8')
|
rom.name = bytearray(f'ER{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{world.seed:09}\0', 'utf8')[:21]
|
||||||
assert len(rom.name) <= 21
|
rom.name.extend([0] * (21 - len(rom.name)))
|
||||||
rom.write_bytes(0x7FC0, rom.name)
|
rom.write_bytes(0x7FC0, rom.name)
|
||||||
|
|
||||||
|
# set player names
|
||||||
|
for p in range(1, min(world.players, 64) + 1):
|
||||||
|
rom.write_bytes(0x186380 + ((p - 1) * 32), hud_format_text(world.player_names[p][team]))
|
||||||
|
|
||||||
# Write title screen Code
|
# Write title screen Code
|
||||||
hashint = int(rom.get_hash(), 16)
|
hashint = int(rom.get_hash(), 16)
|
||||||
code = [
|
code = [
|
||||||
@@ -1275,6 +1291,7 @@ def patch_rom(world, player, rom, enemized):
|
|||||||
hashint & 0x1F,
|
hashint & 0x1F,
|
||||||
]
|
]
|
||||||
rom.write_bytes(0x180215, code)
|
rom.write_bytes(0x180215, code)
|
||||||
|
rom.hash = code
|
||||||
|
|
||||||
return rom
|
return rom
|
||||||
|
|
||||||
@@ -1338,7 +1355,7 @@ def hud_format_text(text):
|
|||||||
return output[:32]
|
return output[:32]
|
||||||
|
|
||||||
|
|
||||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes, names = None):
|
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, ow_palettes, uw_palettes):
|
||||||
if sprite and not isinstance(sprite, Sprite):
|
if sprite and not isinstance(sprite, Sprite):
|
||||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
|
sprite = Sprite(sprite) if os.path.isfile(sprite) else get_sprite_from_name(sprite)
|
||||||
|
|
||||||
@@ -1407,11 +1424,6 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||||||
elif uw_palettes == 'blackout':
|
elif uw_palettes == 'blackout':
|
||||||
blackout_uw_palettes(rom)
|
blackout_uw_palettes(rom)
|
||||||
|
|
||||||
# set player names
|
|
||||||
for player, name in names.items():
|
|
||||||
if 0 < player <= 64:
|
|
||||||
rom.write_bytes(0x186380 + ((player - 1) * 32), hud_format_text(name))
|
|
||||||
|
|
||||||
if isinstance(rom, LocalRom):
|
if isinstance(rom, LocalRom):
|
||||||
rom.write_crc()
|
rom.write_crc()
|
||||||
|
|
||||||
@@ -1548,12 +1560,15 @@ def blackout_uw_palettes(rom):
|
|||||||
rom.write_bytes(i+44, [0] * 76)
|
rom.write_bytes(i+44, [0] * 76)
|
||||||
rom.write_bytes(i+136, [0] * 44)
|
rom.write_bytes(i+136, [0] * 44)
|
||||||
|
|
||||||
|
def get_hash_string(hash):
|
||||||
|
return ", ".join([hash_alphabet[code & 0x1F] for code in hash])
|
||||||
|
|
||||||
def write_string_to_rom(rom, target, string):
|
def write_string_to_rom(rom, target, string):
|
||||||
address, maxbytes = text_addresses[target]
|
address, maxbytes = text_addresses[target]
|
||||||
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
|
||||||
|
|
||||||
|
|
||||||
def write_strings(rom, world, player):
|
def write_strings(rom, world, player, team):
|
||||||
tt = TextTable()
|
tt = TextTable()
|
||||||
tt.removeUnwantedText()
|
tt.removeUnwantedText()
|
||||||
|
|
||||||
@@ -1571,11 +1586,11 @@ def write_strings(rom, world, player):
|
|||||||
hint = dest.hint_text if dest.hint_text else "something"
|
hint = dest.hint_text if dest.hint_text else "something"
|
||||||
if dest.player != player:
|
if dest.player != player:
|
||||||
if ped_hint:
|
if ped_hint:
|
||||||
hint += " for p%d!" % dest.player
|
hint += f" for {world.player_names[dest.player][team]}!"
|
||||||
elif type(dest) in [Region, Location]:
|
elif type(dest) in [Region, Location]:
|
||||||
hint += " in p%d's world" % dest.player
|
hint += f" in {world.player_names[dest.player][team]}'s world"
|
||||||
else:
|
else:
|
||||||
hint += " for p%d" % dest.player
|
hint += f" for {world.player_names[dest.player][team]}"
|
||||||
return hint
|
return hint
|
||||||
|
|
||||||
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||||
@@ -2066,9 +2081,9 @@ def patch_shuffled_dark_sanc(world, rom, player):
|
|||||||
rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00])
|
rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00])
|
||||||
|
|
||||||
|
|
||||||
# 24B116 and 20BA72
|
# 24B116 and 20BAD8
|
||||||
compass_r_addr = 0x123116 # a9 90 24 8f 9a c7 7e
|
compass_r_addr = 0x123116 # a9 90 24 8f 9a c7 7e
|
||||||
compass_w_addr = 0x103a72 # e2 20 ad 0c 04 c9 00 d0
|
compass_w_addr = 0x103ad8 # e2 20 ad 0c 04 c9 00 d0
|
||||||
|
|
||||||
|
|
||||||
def compass_code_good(rom):
|
def compass_code_good(rom):
|
||||||
@@ -2366,3 +2381,9 @@ BigKeys = ['Big Key (Eastern Palace)',
|
|||||||
'Big Key (Turtle Rock)',
|
'Big Key (Turtle Rock)',
|
||||||
'Big Key (Ganons Tower)'
|
'Big Key (Ganons Tower)'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hash_alphabet = [
|
||||||
|
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
|
||||||
|
"Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
|
||||||
|
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
|
||||||
|
]
|
||||||
|
|||||||
@@ -369,9 +369,10 @@ def global_rules(world, player):
|
|||||||
|
|
||||||
def default_rules(world, player):
|
def default_rules(world, player):
|
||||||
if world.mode[player] == 'standard':
|
if world.mode[player] == 'standard':
|
||||||
|
# Links house requires reaching Sanc so skipping that chest isn't a softlock.
|
||||||
world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True
|
world.get_region('Hyrule Castle Secret Entrance', player).can_reach_private = lambda state: True
|
||||||
old_rule = world.get_region('Links House', player).can_reach_private
|
old_rule = world.get_region('Links House', player).can_reach_private
|
||||||
world.get_region('Links House', player).can_reach_private = lambda state: state.can_reach('Sanctuary', 'Region', player) or old_rule(state)
|
world.get_region('Links House', player).can_reach_private = lambda state: state.has('Zelda Delivered', player) or old_rule(state)
|
||||||
else:
|
else:
|
||||||
# these are default save&quit points and always accessible
|
# these are default save&quit points and always accessible
|
||||||
world.get_region('Links House', player).can_reach_private = lambda state: True
|
world.get_region('Links House', player).can_reach_private = lambda state: True
|
||||||
@@ -644,21 +645,21 @@ def inverted_rules(world, player):
|
|||||||
|
|
||||||
def no_glitches_rules(world, player):
|
def no_glitches_rules(world, player):
|
||||||
if world.mode[player] != 'inverted':
|
if world.mode[player] != 'inverted':
|
||||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player))
|
add_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player))
|
||||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
|
add_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
|
||||||
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
|
add_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
add_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player) and (state.has('Hammer', player) or state.can_lift_rocks(player)))
|
add_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player) and (state.has('Hammer', player) or state.can_lift_rocks(player)))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
add_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||||
else:
|
else:
|
||||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has_Pearl(player) and (state.has('Flippers', player) or state.can_lift_rocks(player)))
|
add_rule(world.get_entrance('Zoras River', player), lambda state: state.has_Pearl(player) and (state.has('Flippers', player) or state.can_lift_rocks(player)))
|
||||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) # can be fake flippered to
|
add_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player)) # can be fake flippered to
|
||||||
set_rule(world.get_entrance('Lake Hylia Island', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
add_rule(world.get_entrance('Lake Hylia Island', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
add_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has_Pearl(player) and state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
|
add_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player) and (state.has('Hammer', player) or state.can_lift_rocks(player)))
|
add_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player) and (state.has('Hammer', player) or state.can_lift_rocks(player)))
|
||||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
|
add_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
|
||||||
set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
|
add_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
|
||||||
|
|
||||||
# todo: move some dungeon rules to no glictes logic - see these for examples
|
# todo: move some dungeon rules to no glictes logic - see these for examples
|
||||||
# add_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
|
# add_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
|
||||||
@@ -800,23 +801,28 @@ def swordless_rules(world, player):
|
|||||||
|
|
||||||
|
|
||||||
def standard_rules(world, player):
|
def standard_rules(world, player):
|
||||||
# add_rule(world.get_entrance('Sewers Door', player), lambda state: state.can_kill_most_things(player))
|
# these are because of rails
|
||||||
|
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player))
|
||||||
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.has('Zelda Delivered', player))
|
||||||
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
|
||||||
|
|
||||||
|
|
||||||
|
# too restrictive for crossed?
|
||||||
def uncle_item_rule(item):
|
def uncle_item_rule(item):
|
||||||
copy_state = CollectionState(world)
|
copy_state = CollectionState(world)
|
||||||
copy_state.collect(item)
|
copy_state.collect(item)
|
||||||
copy_state.sweep_for_events()
|
copy_state.sweep_for_events()
|
||||||
return copy_state.can_reach('Sanctuary', 'Region', player)
|
return copy_state.has('Zelda Delivered', player)
|
||||||
|
|
||||||
add_item_rule(world.get_location('Link\'s Uncle', player), uncle_item_rule)
|
add_item_rule(world.get_location('Link\'s Uncle', player), uncle_item_rule)
|
||||||
|
|
||||||
# easiest way to enforce key placement not relevant for open
|
# ensures the required weapon for escape lands on uncle (unless player has it pre-equipped)
|
||||||
set_rule(world.get_location('Sewers - Dark Cross', player), lambda state: state.can_kill_most_things(player))
|
for location in ['Link\'s House', 'Sanctuary', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle',
|
||||||
|
'Sewers - Secret Room - Right']:
|
||||||
|
add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player))
|
||||||
|
add_rule(world.get_location('Secret Passage', player), lambda state: state.can_kill_most_things(player))
|
||||||
|
|
||||||
|
# todo: in crossed these chest/key drops are not necessarily present
|
||||||
|
add_rule(world.get_location('Hyrule Castle - Map Chest', player), lambda state: state.can_kill_most_things(player))
|
||||||
|
set_rule(world.get_location('Sewers - Dark Cross', player), lambda state: state.can_kill_most_things(player))
|
||||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.can_kill_most_things(player))
|
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.can_kill_most_things(player))
|
||||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.can_kill_most_things(player))
|
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.can_kill_most_things(player))
|
||||||
|
|
||||||
@@ -825,6 +831,31 @@ def standard_rules(world, player):
|
|||||||
set_rule(world.get_location('Hyrule Castle - Key Rat Key Drop', player), lambda state: state.can_kill_most_things(player))
|
set_rule(world.get_location('Hyrule Castle - Key Rat Key Drop', player), lambda state: state.can_kill_most_things(player))
|
||||||
set_rule(world.get_entrance('Hyrule Dungeon Armory S', player), lambda state: state.can_kill_most_things(player))
|
set_rule(world.get_entrance('Hyrule Dungeon Armory S', player), lambda state: state.can_kill_most_things(player))
|
||||||
|
|
||||||
|
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state.can_kill_most_things(player))
|
||||||
|
set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player))
|
||||||
|
set_rule(world.get_entrance('Hyrule Castle Throne Room N', player), lambda state: state.has('Zelda Herself', player))
|
||||||
|
set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player))
|
||||||
|
|
||||||
|
for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest']:
|
||||||
|
add_rule(world.get_location(location, player), lambda state: state.has('Zelda Delivered', player))
|
||||||
|
|
||||||
|
# Bonk Fairy (Light) is a notable omission in ER shuffles/Retro
|
||||||
|
for entrance in ['Blinds Hideout', 'Zoras River', 'Kings Grave Outer Rocks', 'Dam', 'Tavern North', 'Chicken House',
|
||||||
|
'Aginahs Cave', 'Sahasrahlas Hut', 'Kakariko Well Drop', 'Kakariko Well Cave', 'Blacksmiths Hut',
|
||||||
|
'Bat Cave Drop Ledge', 'Bat Cave Cave', 'Sick Kids House', 'Hobo Bridge',
|
||||||
|
'Lost Woods Hideout Drop', 'Lost Woods Hideout Stump', 'Lumberjack Tree Tree',
|
||||||
|
'Lumberjack Tree Cave', 'Mini Moldorm Cave', 'Ice Rod Cave', 'Lake Hylia Central Island Pier',
|
||||||
|
'Bonk Rock Cave', 'Library', 'Potion Shop', 'Two Brothers House (East)', 'Desert Palace Stairs',
|
||||||
|
'Eastern Palace', 'Master Sword Meadow', 'Sanctuary', 'Sanctuary Grave',
|
||||||
|
'Death Mountain Entrance Rock', 'Flute Spot 1', 'Dark Desert Teleporter', 'East Hyrule Teleporter',
|
||||||
|
'South Hyrule Teleporter', 'Kakariko Teleporter', 'Elder House (East)', 'Elder House (West)',
|
||||||
|
'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)',
|
||||||
|
'Snitch Lady (West)', 'Tavern (Front)', 'Bush Covered House', 'Light World Bomb Hut',
|
||||||
|
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)',
|
||||||
|
'Waterfall of Wishing', 'Hyrule Castle Main Gate', '50 Rupee Cave',
|
||||||
|
'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy',
|
||||||
|
'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']:
|
||||||
|
add_rule(world.get_entrance(entrance, player), lambda state: state.has('Zelda Delivered', player))
|
||||||
|
|
||||||
def set_trock_key_rules(world, player):
|
def set_trock_key_rules(world, player):
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
def parse_names_string(names):
|
|
||||||
return {player: name for player, name in enumerate([n for n in re.split(r'[, ]', names) if n], 1)}
|
|
||||||
|
|
||||||
def int16_as_bytes(value):
|
def int16_as_bytes(value):
|
||||||
value = value & 0xFFFF
|
value = value & 0xFFFF
|
||||||
return [value & 0xFF, (value >> 8) & 0xFF]
|
return [value & 0xFF, (value >> 8) & 0xFF]
|
||||||
@@ -21,6 +18,18 @@ def pc_to_snes(value):
|
|||||||
def snes_to_pc(value):
|
def snes_to_pc(value):
|
||||||
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
return ((value & 0x7F0000)>>1)|(value & 0x7FFF)
|
||||||
|
|
||||||
|
def parse_player_names(names, players, teams):
|
||||||
|
names = [n for n in re.split(r'[, ]', names) if n]
|
||||||
|
ret = []
|
||||||
|
while names or len(ret) < teams:
|
||||||
|
team = [n[:16] for n in names[:players]]
|
||||||
|
while len(team) != players:
|
||||||
|
team.append(f"Player {len(team) + 1}")
|
||||||
|
ret.append(team)
|
||||||
|
|
||||||
|
names = names[players:]
|
||||||
|
return ret
|
||||||
|
|
||||||
def is_bundled():
|
def is_bundled():
|
||||||
return getattr(sys, 'frozen', False)
|
return getattr(sys, 'frozen', False)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ incsrc normal.asm
|
|||||||
incsrc spiral.asm
|
incsrc spiral.asm
|
||||||
incsrc gfx.asm
|
incsrc gfx.asm
|
||||||
incsrc keydoors.asm
|
incsrc keydoors.asm
|
||||||
|
incsrc overrides.asm
|
||||||
|
warnpc $279000
|
||||||
|
|
||||||
; Data Section
|
; Data Section
|
||||||
org $279000
|
org $279000
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ org $1bece4
|
|||||||
Palette_SpriteAux1:
|
Palette_SpriteAux1:
|
||||||
|
|
||||||
|
|
||||||
|
org $0DFA53
|
||||||
|
jsl.l LampCheckOverride
|
||||||
|
|
||||||
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
|
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
|
||||||
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
|
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
|
||||||
; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds
|
; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
;================================================================================
|
||||||
|
; Lamp Mantle & Light Cone Fix
|
||||||
|
;--------------------------------------------------------------------------------
|
||||||
|
; Output: 0 for darkness, 1 for lamp cone
|
||||||
|
;--------------------------------------------------------------------------------
|
||||||
|
LampCheckOverride:
|
||||||
|
LDA $7F50C4 : CMP.b #$01 : BNE + : RTL : +
|
||||||
|
CMP.b #$FF : BNE + : INC : RTL : +
|
||||||
|
|
||||||
|
LDA $7EF34A : BNE .done ; skip if we already have lantern
|
||||||
|
|
||||||
|
LDA $7EF3CA : BNE +
|
||||||
|
.lightWorld
|
||||||
|
LDA $040C : CMP.b #$02 : BNE ++ ; check if we're in HC
|
||||||
|
LDA LampConeSewers : BRA .done
|
||||||
|
++
|
||||||
|
LDA LampConeLightWorld : BRA .done
|
||||||
|
+
|
||||||
|
.darkWorld
|
||||||
|
LDA LampConeDarkWorld
|
||||||
|
.done
|
||||||
|
;BNE + : STZ $1D : + ; remember to turn cone off after a torch
|
||||||
|
RTL
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
|||||||
|
aioconsole==0.1.15
|
||||||
|
colorama==0.4.3
|
||||||
|
websockets==8.1
|
||||||
Reference in New Issue
Block a user