Merge branch 'master' into multiworld
This commit is contained in:
commit
1c6e4c11c6
|
@ -20,5 +20,9 @@ weights/
|
|||
/Players/
|
||||
/QUsb2Snes/
|
||||
/options.yaml
|
||||
/config.yaml
|
||||
/uploads/
|
||||
/logs/
|
||||
_persistent_storage.yaml
|
||||
mystery_result_*.yaml
|
||||
/db.db3
|
||||
|
|
|
@ -13,9 +13,6 @@ def adjust(args):
|
|||
|
||||
if os.stat(args.rom).st_size in (0x200000, 0x400000) and os.path.splitext(args.rom)[-1].lower() == '.sfc':
|
||||
rom = LocalRom(args.rom, patch=False)
|
||||
if os.path.isfile(args.baserom):
|
||||
baserom = LocalRom(args.baserom, patch=True)
|
||||
rom.orig_buffer = baserom.orig_buffer
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
|
||||
|
|
|
@ -102,7 +102,6 @@ class World(object):
|
|||
set_player_attr('enemy_health', 'default')
|
||||
set_player_attr('enemy_damage', 'default')
|
||||
set_player_attr('beemizer', 0)
|
||||
set_player_attr('progressive', 'on')
|
||||
set_player_attr('escape_assist', [])
|
||||
set_player_attr('crystals_needed_for_ganon', 7)
|
||||
set_player_attr('crystals_needed_for_gt', 7)
|
||||
|
@ -114,8 +113,13 @@ class World(object):
|
|||
set_player_attr('glitch_boots', True)
|
||||
set_player_attr('progression_balancing', True)
|
||||
set_player_attr('local_items', set())
|
||||
set_player_attr('triforce_pieces_available', 30)
|
||||
set_player_attr('triforce_pieces_required', 20)
|
||||
|
||||
@property
|
||||
def player_ids(self):
|
||||
yield from range(1, self.players + 1)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||
|
||||
|
@ -250,10 +254,10 @@ class World(object):
|
|||
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
||||
ret.prog_items['Blue Shield', item.player] += 1
|
||||
elif 'Bow' in item.name:
|
||||
if ret.has('Silver Arrows', item.player):
|
||||
if ret.has('Silver', item.player):
|
||||
pass
|
||||
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
||||
ret.prog_items['Silver Arrows', item.player] += 1
|
||||
ret.prog_items['Silver Bow', item.player] += 1
|
||||
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
||||
ret.prog_items['Bow', item.player] += 1
|
||||
elif item.name.startswith('Bottle'):
|
||||
|
@ -268,10 +272,19 @@ class World(object):
|
|||
if keys:
|
||||
for p in range(1, self.players + 1):
|
||||
from Items import ItemFactory
|
||||
for item in ItemFactory(['Small Key (Escape)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', 'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)', 'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
|
||||
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + ['Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + ['Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
|
||||
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + ['Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)', 'Big Key (Ganons Tower)'] + ['Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + ['Small Key (Ganons Tower)'] * 4,
|
||||
p):
|
||||
for item in ItemFactory(
|
||||
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
|
||||
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
|
||||
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
|
||||
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
|
||||
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
|
||||
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
|
||||
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
|
||||
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
|
||||
'Big Key (Ganons Tower)'] + [
|
||||
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
|
||||
'Small Key (Ganons Tower)'] * 4,
|
||||
p):
|
||||
soft_collect(item)
|
||||
ret.sweep_for_events()
|
||||
return ret
|
||||
|
@ -423,26 +436,23 @@ class CollectionState(object):
|
|||
queue.extend(start.exits)
|
||||
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
while True:
|
||||
try:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in rrp:
|
||||
bc.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
rrp.add(new_region)
|
||||
bc.remove(connection)
|
||||
bc.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
while queue:
|
||||
connection = queue.popleft()
|
||||
new_region = connection.connected_region
|
||||
if new_region in rrp:
|
||||
bc.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
rrp.add(new_region)
|
||||
bc.remove(connection)
|
||||
bc.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
if new_region.name in indirect_connections:
|
||||
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
||||
if new_entrance in bc and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
except IndexError:
|
||||
break
|
||||
# Retry connections if the new region can unblock them
|
||||
if new_region.name in indirect_connections:
|
||||
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
||||
if new_entrance in bc and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
|
||||
def copy(self) -> CollectionState:
|
||||
ret = CollectionState(self.world)
|
||||
|
@ -455,7 +465,7 @@ class CollectionState(object):
|
|||
ret.locations_checked = copy.copy(self.locations_checked)
|
||||
return ret
|
||||
|
||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||
def can_reach(self, spot, resolution_hint=None, player=None) -> bool:
|
||||
if not hasattr(spot, "spot_type"):
|
||||
# try to resolve a name
|
||||
if resolution_hint == 'Location':
|
||||
|
@ -467,7 +477,7 @@ class CollectionState(object):
|
|||
spot = self.world.get_region(spot, player)
|
||||
return spot.can_reach(self)
|
||||
|
||||
def sweep_for_events(self, key_only=False, locations=None):
|
||||
def sweep_for_events(self, key_only: bool = False, locations=None):
|
||||
# this may need improvement
|
||||
if locations is None:
|
||||
locations = self.world.get_filled_locations()
|
||||
|
@ -475,7 +485,9 @@ class CollectionState(object):
|
|||
checked_locations = 0
|
||||
while new_locations:
|
||||
reachable_events = [location for location in locations if location.event and
|
||||
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
(not key_only or (not self.world.keyshuffle[
|
||||
location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[
|
||||
location.item.player] and location.item.bigkey))
|
||||
and location.can_reach(self)]
|
||||
for event in reachable_events:
|
||||
if (event.name, event.player) not in self.events:
|
||||
|
@ -484,12 +496,12 @@ class CollectionState(object):
|
|||
new_locations = len(reachable_events) > checked_locations
|
||||
checked_locations = len(reachable_events)
|
||||
|
||||
def has(self, item, player, count=1):
|
||||
def has(self, item, player: int, count: int = 1):
|
||||
if count == 1:
|
||||
return (item, player) in self.prog_items
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def has_key(self, item, player, count=1):
|
||||
def has_key(self, item, player, count: int = 1):
|
||||
if self.world.retro[player]:
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
if count == 1:
|
||||
|
@ -505,6 +517,9 @@ class CollectionState(object):
|
|||
def item_count(self, item, player: int) -> int:
|
||||
return self.prog_items[item, player]
|
||||
|
||||
def has_triforce_pieces(self, count: int, player: int) -> bool:
|
||||
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
|
||||
|
||||
def has_crystals(self, count: int, player: int) -> bool:
|
||||
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
|
||||
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
|
||||
|
@ -561,8 +576,8 @@ class CollectionState(object):
|
|||
def can_shoot_arrows(self, player: int) -> bool:
|
||||
if self.world.retro[player]:
|
||||
# TODO: Progressive and Non-Progressive silvers work differently (progressive is not usable until the shop arrow is bought)
|
||||
return self.has('Bow', player) and self.can_buy_unlimited('Single Arrow', player)
|
||||
return self.has('Bow', player)
|
||||
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy_unlimited('Single Arrow', player)
|
||||
return self.has('Bow', player) or self.has('Silver Bow', player)
|
||||
|
||||
def can_get_good_bee(self, player: int) -> bool:
|
||||
cave = self.world.get_region('Good Bee Cave', player)
|
||||
|
@ -697,10 +712,10 @@ class CollectionState(object):
|
|||
self.prog_items['Blue Shield', item.player] += 1
|
||||
changed = True
|
||||
elif 'Bow' in item.name:
|
||||
if self.has('Silver Arrows', item.player):
|
||||
if self.has('Silver Bow', item.player):
|
||||
pass
|
||||
elif self.has('Bow', item.player):
|
||||
self.prog_items['Silver Arrows', item.player] += 1
|
||||
self.prog_items['Silver Bow', item.player] += 1
|
||||
changed = True
|
||||
else:
|
||||
self.prog_items['Bow', item.player] += 1
|
||||
|
@ -751,8 +766,8 @@ class CollectionState(object):
|
|||
else:
|
||||
to_remove = 'None'
|
||||
elif 'Bow' in item.name:
|
||||
if self.has('Silver Arrows', item.player):
|
||||
to_remove = 'Silver Arrows'
|
||||
if self.has('Silver Bow', item.player):
|
||||
to_remove = 'Silver Bow'
|
||||
elif self.has('Bow', item.player):
|
||||
to_remove = 'Bow'
|
||||
else:
|
||||
|
@ -817,7 +832,7 @@ class Region(object):
|
|||
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
||||
or (item.map and not self.world.mapshuffle[item.player])
|
||||
or (item.compass and not self.world.compassshuffle[item.player]))
|
||||
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
||||
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)'
|
||||
if sewer_hack or inside_dungeon_item:
|
||||
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
|
@ -937,11 +952,11 @@ class Location(object):
|
|||
self.item_rule = lambda item: True
|
||||
self.player = player
|
||||
|
||||
def can_fill(self, state, item, check_access=True) -> bool:
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (
|
||||
not check_access or self.can_reach(state)))
|
||||
|
||||
def can_reach(self, state) -> bool:
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
||||
return True
|
||||
return False
|
||||
|
@ -1195,6 +1210,7 @@ class Spoiler(object):
|
|||
'players': self.world.players,
|
||||
'teams': self.world.teams,
|
||||
'progression_balancing': self.world.progression_balancing,
|
||||
'triforce_pieces_available': self.world.triforce_pieces_available,
|
||||
'triforce_pieces_required': self.world.triforce_pieces_required,
|
||||
}
|
||||
|
||||
|
@ -1241,6 +1257,8 @@ class Spoiler(object):
|
|||
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
||||
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||
if "triforce" in self.metadata["goal"][player]: # triforce hunt
|
||||
outfile.write(
|
||||
"Pieces available for Triforce: %s\n" % self.metadata['triforce_pieces_available'][player])
|
||||
outfile.write(
|
||||
"Pieces required for Triforce: %s\n" % self.metadata["triforce_pieces_required"][player])
|
||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||
|
|
29
Dungeons.py
29
Dungeons.py
|
@ -15,13 +15,17 @@ def create_dungeons(world, player):
|
|||
dungeon.world = world
|
||||
return dungeon
|
||||
|
||||
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'], None, [ItemFactory('Small Key (Escape)', player)], [ItemFactory('Map (Escape)', player)])
|
||||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'], ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
ES = make_dungeon('Hyrule Castle', None, ['Hyrule Castle', 'Sewers', 'Sewer Drop', 'Sewers (Dark)', 'Sanctuary'],
|
||||
None, [ItemFactory('Small Key (Hyrule Castle)', player)],
|
||||
[ItemFactory('Map (Hyrule Castle)', player)])
|
||||
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
|
||||
ItemFactory('Big Key (Eastern Palace)', player), [],
|
||||
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
|
||||
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
|
||||
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
|
||||
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
|
||||
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 2, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 3, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
|
||||
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
|
||||
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
|
||||
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
|
||||
|
@ -46,15 +50,6 @@ def fill_dungeons(world):
|
|||
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
if world.retro[player]:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
|
||||
else:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
|
||||
pinball_room.event = True
|
||||
pinball_room.locked = True
|
||||
|
||||
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
|
||||
|
||||
loopcnt = 0
|
||||
|
@ -125,16 +120,6 @@ def get_dungeon_item_pool(world):
|
|||
def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
pinball_room = world.get_location('Skull Woods - Pinball Room', player)
|
||||
if world.retro[player]:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Universal)', player), False)
|
||||
else:
|
||||
world.push_item(pinball_room, ItemFactory('Small Key (Skull Woods)', player), False)
|
||||
pinball_room.event = True
|
||||
pinball_room.locked = True
|
||||
shuffled_locations.remove(pinball_room)
|
||||
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
|
|
|
@ -8,7 +8,7 @@ import textwrap
|
|||
import shlex
|
||||
import sys
|
||||
|
||||
from Main import main
|
||||
from Main import main, get_seed
|
||||
from Rom import get_sprite_from_name
|
||||
from Utils import is_bundled, close_console
|
||||
|
||||
|
@ -67,7 +67,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
Vanilla: Swords are in vanilla locations.
|
||||
''')
|
||||
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?',
|
||||
choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'crystals'],
|
||||
choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals'],
|
||||
help='''\
|
||||
Select completion goal. (default: %(default)s)
|
||||
Ganon: Collect all crystals, beat Agahnim 2 then
|
||||
|
@ -79,10 +79,17 @@ def parse_arguments(argv, no_defaults=False):
|
|||
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
20 of them to beat the game.
|
||||
Local Triforce Hunt: Places 30 Triforce Pieces in your world, collect
|
||||
20 of them to beat the game.
|
||||
20 of them to beat the game.
|
||||
Ganon Triforce Hunt: Places 30 Triforce Pieces in the world, collect
|
||||
20 of them, then defeat Ganon.
|
||||
Local Ganon Triforce Hunt: Places 30 Triforce Pieces in your world,
|
||||
collect 20 of them, then defeat Ganon.
|
||||
''')
|
||||
parser.add_argument('--triforce_pieces_available', default=defval(30),
|
||||
type=lambda value: min(max(int(value), 1), 90),
|
||||
help='''Set Triforce Pieces available in item pool.''')
|
||||
parser.add_argument('--triforce_pieces_required', default=defval(20),
|
||||
type=lambda value: min(max(int(value), 1), 30),
|
||||
type=lambda value: min(max(int(value), 1), 90),
|
||||
help='''Set Triforce Pieces required to win a Triforce Hunt''')
|
||||
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?',
|
||||
choices=['normal', 'hard', 'expert'],
|
||||
|
@ -280,17 +287,15 @@ def parse_arguments(argv, no_defaults=False):
|
|||
''')
|
||||
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
|
||||
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
|
||||
parser.add_argument('--jsonout', action='store_true', help='''\
|
||||
Output .json patch to stdout instead of a patched rom. Used
|
||||
for VT site integration, do not use otherwise.
|
||||
''')
|
||||
parser.add_argument('--skip_progression_balancing', action='store_true', default=defval(False),
|
||||
help="Skip Multiworld Progression balancing.")
|
||||
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
|
||||
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
|
||||
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
|
||||
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
|
||||
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
parser.add_argument('--shuffleenemies', default=defval('none'),
|
||||
choices=['none', 'shuffled', 'chaos', 'chaosthieves'])
|
||||
parser.add_argument('--enemy_health', default=defval('default'),
|
||||
choices=['default', 'easy', 'normal', 'hard', 'expert'])
|
||||
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('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
|
||||
|
@ -302,8 +307,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\
|
||||
create a binary patch file from which the randomized rom can be recreated using MultiClient.
|
||||
Does not work with jsonout.''')
|
||||
create a binary patch file from which the randomized rom can be recreated using MultiClient.''')
|
||||
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
|
||||
turns off starting with Pegasus Boots in glitched modes.''')
|
||||
|
||||
|
@ -333,7 +337,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
'local_items', 'retro', 'accessibility', 'hints', 'beemizer',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'heartbeep', "skip_progression_balancing", "triforce_pieces_required",
|
||||
'heartbeep', "skip_progression_balancing", "triforce_pieces_available", "triforce_pieces_required",
|
||||
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
|
@ -357,15 +361,14 @@ def start():
|
|||
sys.exit(0)
|
||||
|
||||
# ToDo: Validate files further than mere existance
|
||||
if not args.jsonout and not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
if not os.path.isfile(args.rom):
|
||||
input(
|
||||
'Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in
|
||||
args.sprite.values()]):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
if any([sprite is not None and not os.path.isfile(sprite) and not get_sprite_from_name(sprite) for sprite in args.sprite.values()]):
|
||||
if not args.jsonout:
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise IOError('Cannot find sprite file at %s' % args.sprite)
|
||||
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
|
||||
|
@ -378,7 +381,7 @@ def start():
|
|||
seed = args.seed
|
||||
for _ in range(args.count):
|
||||
main(seed=seed, args=args)
|
||||
seed = random.randint(0, 999999999)
|
||||
seed = get_seed()
|
||||
else:
|
||||
main(seed=args.seed, args=args)
|
||||
|
||||
|
|
10
Fill.py
10
Fill.py
|
@ -244,8 +244,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||
continue
|
||||
|
||||
gftower_trash_count = (
|
||||
random.randint(15, 50) if world.goal[player] in {'triforcehunt', 'localtriforcehunt'} else random.randint(0,
|
||||
15))
|
||||
random.randint(15, 50) if 'triforcehunt' in world.goal[player]
|
||||
else random.randint(0, 15))
|
||||
|
||||
gtower_locations = [location for location in fill_locations if
|
||||
'Ganons Tower' in location.name and location.player == player]
|
||||
|
@ -274,21 +274,21 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
|||
|
||||
# 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
|
||||
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and world.mode[item.player] == 'standard' and
|
||||
world.keyshuffle[item.player] else 0)
|
||||
|
||||
fill_restrictive(world, world.state, fill_locations, progitempool)
|
||||
|
||||
if any(
|
||||
localprioitempool.values() or localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
|
||||
for player, items in localprioitempool.items(): # already shuffled
|
||||
for player, items in localprioitempool.items(): # items already shuffled
|
||||
local_locations = [location for location in fill_locations if location.player == player]
|
||||
random.shuffle(local_locations)
|
||||
for item_to_place in items:
|
||||
spot_to_fill = local_locations.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill)
|
||||
for player, items in localrestitempool.items(): # already shuffled
|
||||
for player, items in localrestitempool.items(): # items already shuffled
|
||||
local_locations = [location for location in fill_locations if location.player == player]
|
||||
random.shuffle(local_locations)
|
||||
for item_to_place in items:
|
||||
|
|
46
Gui.py
46
Gui.py
|
@ -16,7 +16,7 @@ ModuleUpdate.update()
|
|||
from AdjusterMain import adjust
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
|
||||
from Main import main, __version__ as ESVersion
|
||||
from Main import main, get_seed, __version__ as ESVersion
|
||||
from Rom import Sprite
|
||||
from Utils import is_bundled, local_path, output_path, open_file
|
||||
|
||||
|
@ -240,7 +240,7 @@ def guiMain(args=None):
|
|||
goalVar = StringVar()
|
||||
goalVar.set('ganon')
|
||||
goalOptionMenu = OptionMenu(goalFrame, goalVar, 'ganon', 'pedestal', 'dungeons', 'triforcehunt',
|
||||
'localtriforcehunt', 'crystals')
|
||||
'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals')
|
||||
goalOptionMenu.pack(side=RIGHT)
|
||||
goalLabel = Label(goalFrame, text='Game goal')
|
||||
goalLabel.pack(side=LEFT)
|
||||
|
@ -327,7 +327,7 @@ def guiMain(args=None):
|
|||
|
||||
shuffleFrame = Frame(drowDownFrame)
|
||||
shuffleVar = StringVar()
|
||||
shuffleVar.set('full')
|
||||
shuffleVar.set('vanilla')
|
||||
shuffleOptionMenu = OptionMenu(shuffleFrame, shuffleVar, 'vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple')
|
||||
shuffleOptionMenu.pack(side=RIGHT)
|
||||
shuffleLabel = Label(shuffleFrame, text='Entrance shuffle algorithm')
|
||||
|
@ -349,10 +349,7 @@ def guiMain(args=None):
|
|||
shuffleFrame.pack(expand=True, anchor=E)
|
||||
|
||||
enemizerFrame = LabelFrame(randomizerWindow, text="Enemizer", padx=5, pady=2)
|
||||
enemizerFrame.columnconfigure(0, weight=1)
|
||||
enemizerFrame.columnconfigure(1, weight=1)
|
||||
enemizerFrame.columnconfigure(2, weight=1)
|
||||
enemizerFrame.columnconfigure(3, weight=1)
|
||||
|
||||
|
||||
enemizerPathFrame = Frame(enemizerFrame)
|
||||
enemizerPathFrame.grid(row=0, column=0, columnspan=3, sticky=W+E, padx=3)
|
||||
|
@ -378,7 +375,7 @@ def guiMain(args=None):
|
|||
enemizerEnemyLabel.pack(side=LEFT)
|
||||
enemyShuffleVar = StringVar()
|
||||
enemyShuffleVar.set('none')
|
||||
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos')
|
||||
enemizerEnemyOption = OptionMenu(enemizerEnemyFrame, enemyShuffleVar, 'none', 'shuffled', 'chaos', 'chaosthieves')
|
||||
enemizerEnemyOption.pack(side=LEFT)
|
||||
|
||||
enemizerBossFrame = Frame(enemizerFrame)
|
||||
|
@ -408,20 +405,21 @@ def guiMain(args=None):
|
|||
enemizerHealthOption = OptionMenu(enemizerHealthFrame, enemizerHealthVar, 'default', 'easy', 'normal', 'hard', 'expert')
|
||||
enemizerHealthOption.pack(side=LEFT)
|
||||
|
||||
bottomFrame = Frame(randomizerWindow, pady=5)
|
||||
multiworldframe = LabelFrame(randomizerWindow, text="Multiworld", padx=5, pady=2)
|
||||
|
||||
worldLabel = Label(bottomFrame, text='Worlds')
|
||||
|
||||
worldLabel = Label(multiworldframe, text='Worlds')
|
||||
worldVar = StringVar()
|
||||
worldSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=worldVar)
|
||||
namesLabel = Label(bottomFrame, text='Player names')
|
||||
worldSpinbox = Spinbox(multiworldframe, from_=1, to=255, width=5, textvariable=worldVar)
|
||||
namesLabel = Label(multiworldframe, text='Player names')
|
||||
namesVar = StringVar()
|
||||
namesEntry = Entry(bottomFrame, textvariable=namesVar)
|
||||
seedLabel = Label(bottomFrame, text='Seed #')
|
||||
namesEntry = Entry(multiworldframe, textvariable=namesVar)
|
||||
seedLabel = Label(multiworldframe, text='Seed #')
|
||||
seedVar = StringVar()
|
||||
seedEntry = Entry(bottomFrame, width=15, textvariable=seedVar)
|
||||
countLabel = Label(bottomFrame, text='Count')
|
||||
seedEntry = Entry(multiworldframe, width=20, textvariable=seedVar)
|
||||
countLabel = Label(multiworldframe, text='Count')
|
||||
countVar = StringVar()
|
||||
countSpinbox = Spinbox(bottomFrame, from_=1, to=100, width=5, textvariable=countVar)
|
||||
countSpinbox = Spinbox(multiworldframe, from_=1, to=100, width=5, textvariable=countVar)
|
||||
|
||||
def generateRom():
|
||||
guiargs = Namespace()
|
||||
|
@ -500,7 +498,7 @@ def guiMain(args=None):
|
|||
seed = guiargs.seed
|
||||
for _ in range(guiargs.count):
|
||||
main(seed=seed, args=guiargs)
|
||||
seed = random.randint(0, 999999999)
|
||||
seed = get_seed()
|
||||
else:
|
||||
main(seed=guiargs.seed, args=guiargs)
|
||||
except Exception as e:
|
||||
|
@ -509,24 +507,24 @@ def guiMain(args=None):
|
|||
else:
|
||||
messagebox.showinfo(title="Success", message="Rom patched successfully")
|
||||
|
||||
generateButton = Button(bottomFrame, text='Generate Patched Rom', command=generateRom)
|
||||
generateButton = Button(farBottomFrame, text='Generate Patched Rom', command=generateRom)
|
||||
|
||||
worldLabel.pack(side=LEFT)
|
||||
worldSpinbox.pack(side=LEFT)
|
||||
namesLabel.pack(side=LEFT)
|
||||
namesEntry.pack(side=LEFT)
|
||||
namesEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
seedLabel.pack(side=LEFT, padx=(5, 0))
|
||||
seedEntry.pack(side=LEFT)
|
||||
countLabel.pack(side=LEFT, padx=(5, 0))
|
||||
countSpinbox.pack(side=LEFT)
|
||||
generateButton.pack(side=LEFT, padx=(5, 0))
|
||||
generateButton.pack(side=RIGHT, padx=(5, 0))
|
||||
|
||||
openOutputButton.pack(side=RIGHT)
|
||||
openOutputButton.pack(side=LEFT)
|
||||
|
||||
drowDownFrame.pack(side=LEFT)
|
||||
rightHalfFrame.pack(side=RIGHT)
|
||||
topFrame.pack(side=TOP)
|
||||
bottomFrame.pack(side=BOTTOM)
|
||||
multiworldframe.pack(side=BOTTOM, expand=True, fill=X)
|
||||
enemizerFrame.pack(side=BOTTOM, fill=BOTH)
|
||||
|
||||
# Adjuster Controls
|
||||
|
@ -1350,7 +1348,7 @@ class SpriteSelector(object):
|
|||
button = Button(frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
|
||||
ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name else ""))
|
||||
button.image = image
|
||||
button.grid(row=i // 16, column=i % 16)
|
||||
button.grid(row=i // 32, column=i % 32)
|
||||
i += 1
|
||||
|
||||
if i == 0:
|
||||
|
|
161
ItemList.py
161
ItemList.py
|
@ -32,7 +32,7 @@ normalfinal25extra = ['Rupees (20)'] * 23 + ['Rupees (5)'] * 2
|
|||
Difficulty = namedtuple('Difficulty',
|
||||
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
|
||||
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
|
||||
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
|
||||
'progressivesword', 'basicsword', 'progressivebow', 'basicbow', 'timedohko', 'timedother',
|
||||
'triforcehunt', 'retro',
|
||||
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
|
||||
'progressive_armor_limit', 'progressive_bottle_limit',
|
||||
|
@ -43,78 +43,82 @@ total_items_to_place = 153
|
|||
difficulties = {
|
||||
'normal': Difficulty(
|
||||
baseitems = normalbaseitems,
|
||||
bottles = normalbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 3,
|
||||
basicshield = ['Blue Shield', 'Red Shield', 'Mirror Shield'],
|
||||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Blue Mail', 'Red Mail'],
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
basicbow = ['Bow', 'Silver Arrows'],
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
retro = ['Small Key (Universal)'] * 17 + ['Rupees (20)'] * 10,
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 4,
|
||||
progressive_shield_limit = 3,
|
||||
progressive_armor_limit = 2,
|
||||
progressive_bow_limit = 2,
|
||||
bottles=normalbottles,
|
||||
bottle_count=4,
|
||||
same_bottle=False,
|
||||
progressiveshield=['Progressive Shield'] * 3,
|
||||
basicshield=['Blue Shield', 'Red Shield', 'Mirror Shield'],
|
||||
progressivearmor=['Progressive Armor'] * 2,
|
||||
basicarmor=['Blue Mail', 'Red Mail'],
|
||||
swordless=['Rupees (20)'] * 4,
|
||||
progressivesword=['Progressive Sword'] * 4,
|
||||
basicsword=['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
progressivebow=["Progressive Bow"] * 2,
|
||||
basicbow=['Bow', 'Silver Bow'],
|
||||
timedohko=['Green Clock'] * 25,
|
||||
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt=['Triforce Piece'] * 30,
|
||||
retro=['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
|
||||
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit=4,
|
||||
progressive_shield_limit=3,
|
||||
progressive_armor_limit=2,
|
||||
progressive_bow_limit=2,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 255,
|
||||
heart_piece_limit = 255,
|
||||
boss_heart_container_limit = 10,
|
||||
heart_piece_limit = 24,
|
||||
),
|
||||
'hard': Difficulty(
|
||||
baseitems = normalbaseitems,
|
||||
bottles = hardbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 3,
|
||||
basicshield = ['Blue Shield', 'Red Shield', 'Red Shield'],
|
||||
progressivearmor = ['Progressive Armor'] * 2,
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 25,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 3,
|
||||
progressive_shield_limit = 2,
|
||||
progressive_armor_limit = 0,
|
||||
progressive_bow_limit = 1,
|
||||
bottles=hardbottles,
|
||||
bottle_count=4,
|
||||
same_bottle=False,
|
||||
progressiveshield=['Progressive Shield'] * 3,
|
||||
basicshield=['Blue Shield', 'Red Shield', 'Red Shield'],
|
||||
progressivearmor=['Progressive Armor'] * 2,
|
||||
basicarmor=['Progressive Armor'] * 2, # neither will count
|
||||
swordless=['Rupees (20)'] * 4,
|
||||
progressivesword=['Progressive Sword'] * 4,
|
||||
basicsword=['Fighter Sword', 'Master Sword', 'Master Sword', 'Tempered Sword'],
|
||||
progressivebow=["Progressive Bow"] * 2,
|
||||
basicbow=['Bow'] * 2,
|
||||
timedohko=['Green Clock'] * 25,
|
||||
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt=['Triforce Piece'] * 30,
|
||||
retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
|
||||
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit=3,
|
||||
progressive_shield_limit=2,
|
||||
progressive_armor_limit=0,
|
||||
progressive_bow_limit=1,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 6,
|
||||
heart_piece_limit = 16,
|
||||
),
|
||||
'expert': Difficulty(
|
||||
baseitems = normalbaseitems,
|
||||
bottles = hardbottles,
|
||||
bottle_count = 4,
|
||||
same_bottle = False,
|
||||
progressiveshield = ['Progressive Shield'] * 3,
|
||||
basicshield = ['Progressive Shield'] * 3, #only the first one will upgrade, making this equivalent to two blue shields
|
||||
progressivearmor = ['Progressive Armor'] * 2, # neither will count
|
||||
basicarmor = ['Progressive Armor'] * 2, # neither will count
|
||||
swordless = ['Rupees (20)'] * 4,
|
||||
progressivesword = ['Progressive Sword'] * 4,
|
||||
basicsword = ['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
basicbow = ['Bow'] * 2,
|
||||
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt = ['Triforce Piece'] * 30,
|
||||
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
|
||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit = 2,
|
||||
progressive_shield_limit = 1,
|
||||
progressive_armor_limit = 0,
|
||||
progressive_bow_limit = 1,
|
||||
bottles=hardbottles,
|
||||
bottle_count=4,
|
||||
same_bottle=False,
|
||||
progressiveshield=['Progressive Shield'] * 3,
|
||||
basicshield=['Progressive Shield'] * 3,
|
||||
# only the first one will upgrade, making this equivalent to two blue shields
|
||||
progressivearmor=['Progressive Armor'] * 2, # neither will count
|
||||
basicarmor=['Progressive Armor'] * 2, # neither will count
|
||||
swordless=['Rupees (20)'] * 4,
|
||||
progressivesword=['Progressive Sword'] * 4,
|
||||
basicsword=['Fighter Sword', 'Fighter Sword', 'Master Sword', 'Master Sword'],
|
||||
progressivebow=["Progressive Bow"] * 2,
|
||||
basicbow=['Bow'] * 2,
|
||||
timedohko=['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||
timedother=['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||
triforcehunt=['Triforce Piece'] * 30,
|
||||
retro=['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 16,
|
||||
extras=[normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||
progressive_sword_limit=2,
|
||||
progressive_shield_limit=1,
|
||||
progressive_armor_limit=0,
|
||||
progressive_bow_limit=1,
|
||||
progressive_bottle_limit = 4,
|
||||
boss_heart_container_limit = 2,
|
||||
heart_piece_limit = 8,
|
||||
|
@ -124,7 +128,7 @@ difficulties = {
|
|||
def generate_itempool(world, player):
|
||||
if world.difficulty[player] not in ['normal', 'hard', 'expert']:
|
||||
raise NotImplementedError(f"Diffulty {world.difficulty[player]}")
|
||||
if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'crystals'}:
|
||||
if world.goal[player] not in {'ganon', 'pedestal', 'dungeons', 'triforcehunt', 'localtriforcehunt', 'ganontriforcehunt', 'localganontriforcehunt', 'crystals'}:
|
||||
raise NotImplementedError(f"Goal {world.goal[player]}")
|
||||
if world.mode[player] not in {'open', 'standard', 'inverted'}:
|
||||
raise NotImplementedError(f"Mode {world.mode[player]}")
|
||||
|
@ -142,9 +146,8 @@ def generate_itempool(world, player):
|
|||
region = world.get_region('Light World', player)
|
||||
|
||||
loc = Location(player, "Murahdahla", parent=region)
|
||||
loc.access_rule = lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star',
|
||||
player) >= \
|
||||
state.world.treasure_hunt_count[player]
|
||||
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
|
||||
|
||||
region.locations.append(loc)
|
||||
world.dynamic_locations.append(loc)
|
||||
|
||||
|
@ -256,7 +259,16 @@ def generate_itempool(world, player):
|
|||
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
|
||||
return item
|
||||
|
||||
world.itempool += [beemizer(item) for item in items]
|
||||
progressionitems = [item for item in items if item.advancement or item.priority or item.type]
|
||||
nonprogressionitems = [beemizer(item) for item in items if not item.advancement and not item.priority and not item.type]
|
||||
random.shuffle(nonprogressionitems)
|
||||
|
||||
triforce_pieces = world.triforce_pieces_available[player]
|
||||
if 'triforcehunt' in world.goal[player] and triforce_pieces > 30:
|
||||
progressionitems += [ItemFactory("Triforce Piece", player)] * (triforce_pieces - 30)
|
||||
nonprogressionitems = nonprogressionitems[(triforce_pieces-30):]
|
||||
|
||||
world.itempool += progressionitems + nonprogressionitems
|
||||
|
||||
# shuffle medallions
|
||||
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
|
@ -460,11 +472,11 @@ def get_pool_core(world, player: int):
|
|||
pool.extend(diff.basicarmor)
|
||||
|
||||
if want_progressives():
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
pool.extend(diff.progressivebow)
|
||||
elif swords != 'swordless':
|
||||
pool.extend(diff.basicbow)
|
||||
else:
|
||||
pool.extend(['Bow', 'Silver Arrows'])
|
||||
pool.extend(['Bow', 'Silver Bow'])
|
||||
|
||||
if swords == 'swordless':
|
||||
pool.extend(diff.swordless)
|
||||
|
@ -501,7 +513,9 @@ def get_pool_core(world, player: int):
|
|||
pool.extend(diff.timedohko)
|
||||
extraitems -= len(diff.timedohko)
|
||||
clock_mode = 'countdown-ohko'
|
||||
if goal in {'triforcehunt', 'localtriforcehunt'}:
|
||||
if 'triforcehunt' in goal:
|
||||
while len(diff.triforcehunt) > world.triforce_pieces_available[player]:
|
||||
diff.triforcehunt.pop()
|
||||
pool.extend(diff.triforcehunt)
|
||||
extraitems -= len(diff.triforcehunt)
|
||||
treasure_hunt_count = world.triforce_pieces_required[player]
|
||||
|
@ -552,7 +566,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
itemtotal = itemtotal + customitemarray[70]
|
||||
|
||||
pool.extend(['Bow'] * customitemarray[0])
|
||||
pool.extend(['Silver Arrows']* customitemarray[1])
|
||||
pool.extend(['Silver Bow']* customitemarray[1])
|
||||
pool.extend(['Blue Boomerang'] * customitemarray[2])
|
||||
pool.extend(['Red Boomerang'] * customitemarray[3])
|
||||
pool.extend(['Hookshot'] * customitemarray[4])
|
||||
|
@ -632,8 +646,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
|||
treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99.
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
|
||||
if (customitemarray[66] < treasure_hunt_count) and (goal in {'triforcehunt', 'localtriforcehunt'}) and (
|
||||
customitemarray[68] == 0):
|
||||
if (customitemarray[66] < treasure_hunt_count) and ('triforcehunt' in goal) and (customitemarray[68] == 0):
|
||||
extrapieces = treasure_hunt_count - customitemarray[66]
|
||||
pool.extend(['Triforce Piece'] * extrapieces)
|
||||
itemtotal = itemtotal + extrapieces
|
||||
|
|
85
Items.py
85
Items.py
|
@ -26,41 +26,42 @@ def ItemFactory(items, player):
|
|||
item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||
'Progressive Bow': (True, False, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Progressive Bow (Alt)': (True, False, None, 0x65, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane','ganon-killing kid', 'ganon doom for sale', 'fungus for pork','archer boy shines again', 'the Silver Arrows'),
|
||||
'Silver Bow': (True, False, None, 0x3B, 'Buy 1 Silver\nget Archery\nfor free.', 'the baconmaker', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Bow'),
|
||||
'Book of Mudora': (True, False, None, 0x1D, 'This is a\nparadox?!', 'and the story book', 'the scholarly kid', 'moon runes for sale', 'drugs for literacy', 'book-worm boy can read again', 'the Book'),
|
||||
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the hammer'),
|
||||
'Hammer': (True, False, None, 0x09, 'stop\nhammer time!', 'and m c hammer', 'hammer-smashing kid', 'm c hammer for sale', 'stop... hammer time', 'stop, hammer time', 'the Hammer'),
|
||||
'Hookshot': (True, False, None, 0x0A, 'BOING!!!\nBOING!!!\nBOING!!!', 'and the tickle beam', 'tickle-monster kid', 'tickle beam for sale', 'witch and tickle boy', 'beam boy tickles again', 'the Hookshot'),
|
||||
'Magic Mirror': (True, False, None, 0x1A, 'Isn\'t your\nreflection so\npretty?', 'the face reflector', 'the narcissistic kid', 'your face for sale', 'trades looking-glass', 'narcissistic boy is happy again', 'the Mirror'),
|
||||
'Flute': (True, False, None, 0x14, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
|
||||
'Pegasus Boots': (True, False, None, 0x4B, 'Gotta go fast!', 'and the sprint shoes', 'the running-man kid', 'sprint shoe for sale', 'shrooms for speed', 'gotta-go-fast boy runs again', 'the Boots'),
|
||||
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the glove'),
|
||||
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the cape'),
|
||||
'Mushroom': (True, False, None, 0x29, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the mushroom'),
|
||||
'Shovel': (True, False, None, 0x13, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the shovel'),
|
||||
'Lamp': (True, False, None, 0x12, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the lamp'),
|
||||
'Magic Powder': (True, False, None, 0x0D, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the powder'),
|
||||
'Moon Pearl': (True, False, None, 0x1F, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the moon pearl'),
|
||||
'Cane of Somaria': (True, False, None, 0x15, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the red cane'),
|
||||
'Fire Rod': (True, False, None, 0x07, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the fire rod'),
|
||||
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the flippers'),
|
||||
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the ice rod'),
|
||||
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the mitts'),
|
||||
'Power Glove': (True, False, None, 0x1B, 'Now you can\nlift weak\nstuff!', 'and the grey mittens', 'body-building kid', 'lift glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'the Glove'),
|
||||
'Cape': (True, False, None, 0x19, 'Wear this to\nbecome\ninvisible!', 'the camouflage cape', 'red riding-hood kid', 'red hood for sale', 'hood from a hood', 'dapper boy hides again', 'the Cape'),
|
||||
'Mushroom': (True, False, None, 0x29, 'I\'m a fun guy!\n\nI\'m a funghi!', 'and the legal drugs', 'the drug-dealing kid', 'legal drugs for sale', 'shroom swap', 'shroom boy sells drugs again', 'the Mushroom'),
|
||||
'Shovel': (True, False, None, 0x13, 'Can\n You\n Dig it?', 'and the spade', 'archaeologist kid', 'dirt spade for sale', 'can you dig it', 'shovel boy digs again', 'the Shovel'),
|
||||
'Lamp': (True, False, None, 0x12, 'Baby, baby,\nbaby.\nLight my way!', 'and the flashlight', 'light-shining kid', 'flashlight for sale', 'fungus for illumination', 'illuminated boy can see again', 'the Lamp'),
|
||||
'Magic Powder': (True, False, None, 0x0D, 'you can turn\nanti-faeries\ninto faeries', 'and the magic sack', 'the sack-holding kid', 'magic sack for sale', 'the witch and assistant', 'magic boy plays marbles again', 'the Powder'),
|
||||
'Moon Pearl': (True, False, None, 0x1F, ' Bunny Link\n be\n gone!', 'and the jaw breaker', 'fortune-telling kid', 'lunar orb for sale', 'shrooms for moon rock', 'moon boy plays ball again', 'the Moon Pearl'),
|
||||
'Cane of Somaria': (True, False, None, 0x15, 'I make blocks\nto hold down\nswitches!', 'and the red blocks', 'the block-making kid', 'block stick for sale', 'block stick for trade', 'cane boy makes blocks again', 'the Red Cane'),
|
||||
'Fire Rod': (True, False, None, 0x07, 'I\'m the hot\nrod. I make\nthings burn!', 'and the flamethrower', 'fire-starting kid', 'rage rod for sale', 'fungus for rage-rod', 'firestarter boy burns again', 'the Fire Rod'),
|
||||
'Flippers': (True, False, None, 0x1E, 'fancy a swim?', 'and the toewebs', 'the swimming kid', 'finger webs for sale', 'shrooms let you swim', 'swimming boy swims again', 'the Flippers'),
|
||||
'Ice Rod': (True, False, None, 0x08, 'I\'m the cold\nrod. I make\nthings freeze!', 'and the freeze ray', 'the ice-bending kid', 'freeze ray for sale', 'fungus for ice-rod', 'ice-cube boy freezes again', 'the Ice Rod'),
|
||||
'Titans Mitts': (True, False, None, 0x1C, 'Now you can\nlift heavy\nstuff!', 'and the golden glove', 'body-building kid', 'carry glove for sale', 'fungus for bling-gloves', 'body-building boy has gold again', 'the Mitts'),
|
||||
'Bombos': (True, False, None, 0x0F, 'Burn, baby,\nburn! Fear my\nring of fire!', 'and the swirly coin', 'coin-collecting kid', 'swirly coin for sale', 'shrooms for swirly-coin', 'medallion boy melts room again', 'Bombos'),
|
||||
'Ether': (True, False, None, 0x10, 'This magic\ncoin freezes\neverything!', 'and the bolt coin', 'coin-collecting kid', 'bolt coin for sale', 'shrooms for bolt-coin', 'medallion boy sees floor again', 'Ether'),
|
||||
'Quake': (True, False, None, 0x11, 'Maxing out the\nRichter scale\nis what I do!', 'and the wavy coin', 'coin-collecting kid', 'wavy coin for sale', 'shrooms for wavy-coin', 'medallion boy shakes dirt again', 'Quake'),
|
||||
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a Bottle'),
|
||||
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a Bottle'),
|
||||
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a Bottle'),
|
||||
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a Bottle'),
|
||||
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a Bottle'),
|
||||
'Bottle (Bee)': (True, False, None, 0x3C, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a Bottle'),
|
||||
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a Bottle'),
|
||||
'Bottle': (True, False, None, 0x16, 'Now you can\nstore potions\nand stuff!', 'and the terrarium', 'the terrarium kid', 'terrarium for sale', 'special promotion', 'bottle boy has terrarium again', 'a bottle'),
|
||||
'Bottle (Red Potion)': (True, False, None, 0x2B, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a bottle'),
|
||||
'Bottle (Green Potion)': (True, False, None, 0x2C, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a bottle'),
|
||||
'Bottle (Blue Potion)': (True, False, None, 0x2D, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a bottle'),
|
||||
'Bottle (Fairy)': (True, False, None, 0x3D, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a bottle'),
|
||||
'Bottle (Bee)': (True, False, None, 0x3C, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bottle'),
|
||||
'Bottle (Good Bee)': (True, False, None, 0x48, 'I will sting your foes a whole lot!', 'and the sparkle sting', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has beetor again', 'a bottle'),
|
||||
'Master Sword': (True, False, 'Sword', 0x50, 'I beat barries and pigs alike', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'),
|
||||
'Tempered Sword': (True, False, 'Sword', 0x02, 'I stole the\nblacksmith\'s\njob!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'),
|
||||
'Fighter Sword': (True, False, 'Sword', 0x49, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the small sword'),
|
||||
'Fighter Sword': (True, False, 'Sword', 0x49, 'A pathetic\nsword rests\nhere!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Small Sword'),
|
||||
'Golden Sword': (True, False, 'Sword', 0x03, 'The butter\nsword rests\nhere!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'),
|
||||
'Progressive Sword': (True, False, 'Sword', 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a sword'),
|
||||
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a glove'),
|
||||
'Silver Arrows': (True, False, None, 0x58, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the silver arrows'),
|
||||
'Progressive Sword': (True, False, 'Sword', 0x5E, 'a better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'),
|
||||
'Progressive Glove': (True, False, None, 0x61, 'a way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'),
|
||||
'Green Pendant': (True, False, 'Crystal', (0x04, 0x38, 0x62, 0x00, 0x69, 0x01), None, None, None, None, None, None, None),
|
||||
'Blue Pendant': (True, False, 'Crystal', (0x02, 0x34, 0x60, 0x00, 0x69, 0x02), None, None, None, None, None, None, None),
|
||||
'Red Pendant': (True, False, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, None),
|
||||
|
@ -83,17 +84,17 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Bombs (10)': (False, False, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
|
||||
'Bomb Upgrade (+10)': (False, False, None, 0x52, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
|
||||
'Bomb Upgrade (+5)': (False, False, None, 0x51, 'increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
|
||||
'Blue Mail': (False, True, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the blue mail'),
|
||||
'Red Mail': (False, True, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the red mail'),
|
||||
'Blue Mail': (False, True, None, 0x22, 'Now you\'re a\nblue elf!', 'and the banana hat', 'the protected kid', 'banana hat for sale', 'the clothing store', 'tailor boy banana hatted again', 'the Blue Mail'),
|
||||
'Red Mail': (False, True, None, 0x23, 'Now you\'re a\nred elf!', 'and the eggplant hat', 'well-protected kid', 'purple hat for sale', 'the nice clothing store', 'tailor boy fears nothing again', 'the Red Mail'),
|
||||
'Progressive Armor': (False, True, None, 0x60, 'time for a\nchange of\nclothes?', 'and the unknown hat', 'the protected kid', 'new hat for sale', 'the clothing store', 'tailor boy has threads again', 'some armor'),
|
||||
'Blue Boomerang': (True, False, None, 0x0C, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the blue boomerang'),
|
||||
'Red Boomerang': (True, False, None, 0x2A, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the red boomerang'),
|
||||
'Blue Shield': (False, True, None, 0x04, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'the blue shield'),
|
||||
'Red Shield': (False, True, None, 0x05, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'the red shield'),
|
||||
'Mirror Shield': (True, False, None, 0x06, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the mirror shield'),
|
||||
'Blue Boomerang': (True, False, None, 0x0C, 'No matter what\nyou do, blue\nreturns to you', 'and the bluemarang', 'the bat-throwing kid', 'bent stick for sale', 'fungus for puma-stick', 'throwing boy plays fetch again', 'the Blue Boomerang'),
|
||||
'Red Boomerang': (True, False, None, 0x2A, 'No matter what\nyou do, red\nreturns to you', 'and the badmarang', 'the bat-throwing kid', 'air foil for sale', 'fungus for return-stick', 'magical boy plays fetch again', 'the Bed Boomerang'),
|
||||
'Blue Shield': (False, True, None, 0x04, 'Now you can\ndefend against\npebbles!', 'and the stone blocker', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'the Blue Shield'),
|
||||
'Red Shield': (False, True, None, 0x05, 'Now you can\ndefend against\nfireballs!', 'and the shot blocker', 'shield-wielding kid', 'fire shield for sale', 'fungus for fire shield', 'shield boy defends again', 'the Red Shield'),
|
||||
'Mirror Shield': (True, False, None, 0x06, 'Now you can\ndefend against\nlasers!', 'and the laser blocker', 'shield-wielding kid', 'face shield for sale', 'fungus for face shield', 'shield boy defends again', 'the Mirror Shield'),
|
||||
'Progressive Shield': (True, False, None, 0x5F, 'have a better\nblocker in\nfront of you', 'and the new shield', 'shield-wielding kid', 'shield for sale', 'fungus for shield', 'shield boy defends again', 'a shield'),
|
||||
'Bug Catching Net': (True, False, None, 0x21, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the bug net'),
|
||||
'Cane of Byrna': (True, False, None, 0x18, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the blue cane'),
|
||||
'Bug Catching Net': (True, False, None, 0x21, 'Let\'s catch\nsome bees and\nfaeries!', 'and the bee catcher', 'the bug-catching kid', 'stick web for sale', 'fungus for butterflies', 'wrong boy catches bees again', 'the Bug Net'),
|
||||
'Cane of Byrna': (True, False, None, 0x18, 'Use this to\nbecome\ninvincible!', 'and the bad cane', 'the spark-making kid', 'spark stick for sale', 'spark-stick for trade', 'cane boy encircles again', 'the Blue Cane'),
|
||||
'Boss Heart Container': (False, False, None, 0x3E, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'),
|
||||
'Sanctuary Heart Container': (False, False, None, 0x3F, 'Maximum health\nincreased!\nYeah!', 'and the full heart', 'the life-giving kid', 'love for sale', 'fungus for life', 'life boy feels love again', 'a heart'),
|
||||
'Piece of Heart': (False, False, None, 0x17, 'Just a little\npiece of love!', 'and the broken heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart piece'),
|
||||
|
@ -109,8 +110,8 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Green Clock': (False, True, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'),
|
||||
'Single RNG': (False, True, None, 0x62, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'),
|
||||
'Multi RNG': (False, True, None, 0x63, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'),
|
||||
'Magic Upgrade (1/2)': (True, False, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'half magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Magic Upgrade (1/4)': (True, False, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'quarter magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Magic Upgrade (1/2)': (True, False, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Magic Upgrade (1/4)': (True, False, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
|
||||
'Small Key (Eastern Palace)': (False, False, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'),
|
||||
'Big Key (Eastern Palace)': (False, False, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'),
|
||||
'Compass (Eastern Palace)': (False, True, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'),
|
||||
|
@ -123,10 +124,10 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Big Key (Tower of Hera)': (False, False, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'),
|
||||
'Compass (Tower of Hera)': (False, True, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'),
|
||||
'Map (Tower of Hera)': (False, True, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'),
|
||||
'Small Key (Escape)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
|
||||
'Big Key (Escape)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
|
||||
'Compass (Escape)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
|
||||
'Map (Escape)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
|
||||
'Small Key (Hyrule Castle)': (False, False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
|
||||
'Big Key (Hyrule Castle)': (False, False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
|
||||
'Compass (Hyrule Castle)': (False, True, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
|
||||
'Map (Hyrule Castle)': (False, True, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
|
||||
'Small Key (Agahnims Tower)': (False, False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
|
||||
# doors-specific items, baserom will not be able to understand these
|
||||
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
|
||||
|
@ -167,7 +168,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla
|
|||
'Map (Ganons Tower)': (False, True, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
||||
'Small Key (Universal)': (False, True, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||
'Nothing': (False, False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
||||
'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'friendship'),
|
||||
'Bee Trap': (False, False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship'),
|
||||
'Red Potion': (False, False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
||||
'Green Potion': (False, False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
||||
'Blue Potion': (False, False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
||||
|
@ -185,7 +186,7 @@ lookup_id_to_name = {data[3]: name for name, data in item_table.items()}
|
|||
|
||||
hint_blacklist = {"Triforce"}
|
||||
|
||||
item_name_groups = {"Bows": {"Bow", "Silver Arrows", "Progressive Bow (Alt)", "Progressive Bow"},
|
||||
item_name_groups = {"Bows": {"Bow", "Silver Arrows", "Silver Bow", "Progressive Bow (Alt)", "Progressive Bow"},
|
||||
"Gloves": {"Power Glove", "Progressive Glove", "Titans Mitts"},
|
||||
"Medallions": {"Ether", "Bombos", "Quake"}}
|
||||
# generic groups, (Name, substring)
|
||||
|
@ -208,3 +209,5 @@ for basename, substring in _simple_groups:
|
|||
tempset.add(itemname)
|
||||
|
||||
del (_simple_groups)
|
||||
|
||||
progression_items = {name for name, data in item_table.items() if type(data[3]) == int and data[0]}
|
||||
|
|
1
LICENSE
1
LICENSE
|
@ -2,6 +2,7 @@ MIT License
|
|||
|
||||
Copyright (c) 2017 LLCoolDave
|
||||
Copyright (c) 2020 Berserker66
|
||||
Copyright (c) 2020 CaitSith2
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
137
Main.py
137
Main.py
|
@ -10,16 +10,26 @@ import zlib
|
|||
|
||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
|
||||
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, lookup_vanilla_location_to_entrance
|
||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from EntranceShuffle import link_entrances, link_inverted_entrances
|
||||
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, JsonRom, get_hash_string
|
||||
from Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
|
||||
from Rules import set_rules
|
||||
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 Utils import output_path, parse_player_names, get_options, __version__
|
||||
|
||||
seeddigits = 20
|
||||
|
||||
|
||||
def get_seed(seed=None):
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
return random.randint(0, pow(10, seeddigits) - 1)
|
||||
return seed
|
||||
|
||||
|
||||
def main(args, seed=None):
|
||||
if args.outputpath:
|
||||
|
@ -33,11 +43,7 @@ def main(args, seed=None):
|
|||
args.item_functionality, args.timer, args.progressive.copy(), args.goal, args.algorithm,
|
||||
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
world.seed = random.randint(0, 999999999)
|
||||
else:
|
||||
world.seed = int(seed)
|
||||
world.seed = get_seed(seed)
|
||||
random.seed(world.seed)
|
||||
|
||||
world.remote_items = args.remote_items.copy()
|
||||
|
@ -58,6 +64,7 @@ def main(args, seed=None):
|
|||
world.progressive = args.progressive.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.glitch_boots = args.glitch_boots.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.progression_balancing = {player: not balance for player, balance in args.skip_progression_balancing.items()}
|
||||
|
||||
|
@ -87,6 +94,8 @@ def main(args, seed=None):
|
|||
world.push_precollected(item)
|
||||
world.local_items[player] = {item.strip() for item in args.local_items[player].split(',')}
|
||||
|
||||
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
create_regions(world, player)
|
||||
else:
|
||||
|
@ -152,10 +161,9 @@ def main(args, seed=None):
|
|||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
outfilebase = 'ER_%s' % (args.outputname if args.outputname else world.seed)
|
||||
outfilebase = 'BM_%s' % (args.outputname if args.outputname else world.seed)
|
||||
|
||||
rom_names = []
|
||||
jsonout = {}
|
||||
|
||||
def _gen_rom(team: int, player: int):
|
||||
sprite_random_on_hit = type(args.sprite[player]) is str and args.sprite[player].lower() == 'randomonhit'
|
||||
|
@ -163,15 +171,13 @@ def main(args, seed=None):
|
|||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or args.shufflepots[player] or sprite_random_on_hit)
|
||||
|
||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
||||
rom = LocalRom(args.rom)
|
||||
|
||||
patch_rom(world, rom, player, team, use_enemizer)
|
||||
|
||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player],
|
||||
if use_enemizer:
|
||||
patch_enemizer(world, player, rom, args.enemizercli,
|
||||
sprite_random_on_hit)
|
||||
if not args.jsonout:
|
||||
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
||||
|
||||
if args.race:
|
||||
patch_race_rom(rom)
|
||||
|
@ -182,49 +188,46 @@ def main(args, seed=None):
|
|||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
args.ow_palettes[player], args.uw_palettes[player])
|
||||
|
||||
if args.jsonout:
|
||||
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||
else:
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-keysanity'
|
||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
|
||||
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||
mcsb_name = ''
|
||||
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-keysanity'
|
||||
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]].count(True) == 1:
|
||||
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[
|
||||
player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
|
||||
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
|
||||
world.bigkeyshuffle[player]]):
|
||||
mcsb_name = '-%s%s%s%sshuffle' % (
|
||||
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
|
||||
'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '')
|
||||
|
||||
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
|
||||
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],
|
||||
"" if world.timer[player] in [False,
|
||||
'display'] else "-" +
|
||||
world.timer[
|
||||
player],
|
||||
world.shuffle[player], world.algorithm,
|
||||
mcsb_name,
|
||||
"-retro" if world.retro[player] else "",
|
||||
"-prog_" + world.progressive[player] if
|
||||
world.progressive[player] in ['off',
|
||||
'random'] else "",
|
||||
"-nohints" if not world.hints[
|
||||
player] else "")) if not args.outputname else ''
|
||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath)
|
||||
if args.create_diff:
|
||||
import Patch
|
||||
Patch.create_patch_file(rompath)
|
||||
outfilepname = f'_T{team + 1}' if world.teams > 1 else ''
|
||||
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],
|
||||
"" if world.timer[player] in [False,
|
||||
'display'] else "-" +
|
||||
world.timer[
|
||||
player],
|
||||
world.shuffle[player], world.algorithm,
|
||||
mcsb_name,
|
||||
"-retro" if world.retro[player] else "",
|
||||
"-prog_" + world.progressive[player] if
|
||||
world.progressive[player] in ['off',
|
||||
'random'] else "",
|
||||
"-nohints" if not world.hints[
|
||||
player] else "")) if not args.outputname else ''
|
||||
rompath = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
|
||||
rom.write_to_file(rompath)
|
||||
if args.create_diff:
|
||||
import Patch
|
||||
Patch.create_patch_file(rompath)
|
||||
return (player, team, list(rom.name))
|
||||
|
||||
if not args.suppress_rom:
|
||||
|
@ -253,7 +256,12 @@ def main(args, seed=None):
|
|||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
precollected_items = [[] for player in range(world.players)]
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player - 1].append(item.code)
|
||||
|
||||
multidata = zlib.compress(json.dumps({"names": parsed_names,
|
||||
"roms": rom_names,
|
||||
|
@ -265,20 +273,17 @@ def main(args, seed=None):
|
|||
type(location.address) is int],
|
||||
"server_options": get_options()["server_options"],
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items
|
||||
}).encode("utf-8"), 9)
|
||||
if args.jsonout:
|
||||
jsonout["multidata"] = list(multidata)
|
||||
else:
|
||||
with open(output_path('%s_multidata' % outfilebase), 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
with open(output_path('%s.multidata' % outfilebase), 'wb') as f:
|
||||
f.write(multidata)
|
||||
|
||||
if not args.skip_playthrough:
|
||||
logger.info('Calculating playthrough.')
|
||||
create_playthrough(world)
|
||||
|
||||
if args.jsonout:
|
||||
print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()}))
|
||||
elif args.create_spoiler:
|
||||
if args.create_spoiler:
|
||||
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
|
|
|
@ -48,9 +48,16 @@ class Context():
|
|||
self.snes_address = snes_address
|
||||
self.server_address = server_address
|
||||
|
||||
# WebUI Stuff
|
||||
self.ui_node = WebUI.WebUiClient()
|
||||
self.custom_address = None
|
||||
self.webui_socket_port: typing.Optional[int] = port
|
||||
self.hint_cost = 0
|
||||
self.check_points = 0
|
||||
self.forfeit_mode = ''
|
||||
self.remaining_mode = ''
|
||||
self.hint_points = 0
|
||||
# End WebUI Stuff
|
||||
|
||||
self.exit_event = asyncio.Event()
|
||||
self.watcher_event = asyncio.Event()
|
||||
|
@ -436,7 +443,7 @@ async def get_snes_devices(ctx: Context):
|
|||
|
||||
|
||||
async def snes_connect(ctx: Context, address):
|
||||
if ctx.snes_socket is not None:
|
||||
if ctx.snes_socket is not None and ctx.snes_state == SNES_CONNECTED:
|
||||
ctx.ui_node.log_error('Already connected to snes')
|
||||
return
|
||||
|
||||
|
@ -475,7 +482,7 @@ async def snes_connect(ctx: Context, address):
|
|||
ctx.snes_attached_device = (devices.index(device), device)
|
||||
ctx.ui_node.send_connection_status(ctx)
|
||||
|
||||
if 'SD2SNES'.lower() in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||
if 'sd2snes' in device.lower() or (len(device) == 4 and device[:3] == 'COM'):
|
||||
ctx.ui_node.log_info("SD2SNES Detected")
|
||||
ctx.is_sd2snes = True
|
||||
await ctx.snes_socket.send(json.dumps({"Opcode" : "Info", "Space" : "SNES"}))
|
||||
|
@ -761,7 +768,13 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
|
||||
logging.info("Forfeit setting: "+args["forfeit_mode"])
|
||||
logging.info("Remaining setting: "+args["remaining_mode"])
|
||||
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']} for each location checked.")
|
||||
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
|
||||
f" for each location checked.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
ctx.check_points = int(args['location_check_points'])
|
||||
ctx.forfeit_mode = args['forfeit_mode']
|
||||
ctx.remaining_mode = args['remaining_mode']
|
||||
ctx.ui_node.send_game_info(ctx)
|
||||
if len(args['players']) < 1:
|
||||
ctx.ui_node.log_info('No player connected')
|
||||
else:
|
||||
|
@ -805,6 +818,8 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
msgs.append(['LocationScouts', list(ctx.locations_scouted)])
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
if ctx.finished_game:
|
||||
await send_finished_game(ctx)
|
||||
|
||||
elif cmd == 'ReceivedItems':
|
||||
start_index, items = args
|
||||
|
@ -880,8 +895,13 @@ async def process_server_cmd(ctx: Context, cmd, args):
|
|||
|
||||
elif cmd == "AliasUpdate":
|
||||
ctx.player_names = {p: n for p, n in args}
|
||||
|
||||
elif cmd == 'Print':
|
||||
ctx.ui_node.log_info(args)
|
||||
|
||||
elif cmd == 'HintPointUpdate':
|
||||
ctx.hint_points = args[0]
|
||||
|
||||
else:
|
||||
logging.debug(f"unknown command {args}")
|
||||
|
||||
|
@ -1045,6 +1065,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
def new_check(location):
|
||||
ctx.locations_checked.add(location)
|
||||
ctx.ui_node.log_info("New check: %s (%d/216)" % (location, len(ctx.locations_checked)))
|
||||
ctx.ui_node.send_location_check(ctx, location)
|
||||
new_locations.append(Regions.location_table[location][0])
|
||||
|
||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||
|
@ -1102,6 +1123,14 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||
await ctx.send_msgs([['LocationChecks', new_locations]])
|
||||
|
||||
|
||||
async def send_finished_game(ctx: Context):
|
||||
try:
|
||||
await ctx.send_msgs([['GameFinished', '']])
|
||||
ctx.finished_game = True
|
||||
except Exception as ex:
|
||||
logging.exception(ex)
|
||||
|
||||
|
||||
async def game_watcher(ctx : Context):
|
||||
prev_game_timer = 0
|
||||
perf_counter = time.perf_counter()
|
||||
|
@ -1141,11 +1170,7 @@ async def game_watcher(ctx : Context):
|
|||
delay = 7 if ctx.slow_mode else 2
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
try:
|
||||
await ctx.send_msgs([['GameFinished', '']])
|
||||
ctx.finished_game = True
|
||||
except Exception as ex:
|
||||
logging.exception(ex)
|
||||
await(send_finished_game(ctx))
|
||||
|
||||
if time.perf_counter() - perf_counter < delay:
|
||||
continue
|
||||
|
@ -1223,6 +1248,10 @@ async def websocket_server(websocket: websockets.WebSocketServerProtocol, path,
|
|||
ctx.ui_node.send_connection_status(ctx)
|
||||
elif data['content'] == 'devices':
|
||||
await get_snes_devices(ctx)
|
||||
elif data['content'] == 'gameInfo':
|
||||
ctx.ui_node.send_game_info(ctx)
|
||||
elif data['content'] == 'checkData':
|
||||
ctx.ui_node.send_location_check(ctx, 'Waiting for check...')
|
||||
|
||||
elif data['type'] == 'webConfig':
|
||||
if 'serverAddress' in data['content']:
|
||||
|
|
|
@ -118,8 +118,8 @@ if __name__ == "__main__":
|
|||
seedname = segment
|
||||
break
|
||||
|
||||
multidataname = f"ER_{seedname}_multidata"
|
||||
spoilername = f"ER_{seedname}_Spoiler.txt"
|
||||
multidataname = f"BM_{seedname}.multidata"
|
||||
spoilername = f"BM_{seedname}_Spoiler.txt"
|
||||
romfilename = ""
|
||||
|
||||
if player_name:
|
||||
|
@ -156,7 +156,7 @@ if __name__ == "__main__":
|
|||
print(f"Removed {file} which is now present in the zipfile")
|
||||
|
||||
|
||||
zipname = os.path.join(output_path, f"ER_{seedname}.{typical_zip_ending}")
|
||||
zipname = os.path.join(output_path, f"BM_{seedname}.{typical_zip_ending}")
|
||||
|
||||
print(f"Creating zipfile {zipname}")
|
||||
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
|
||||
|
|
305
MultiServer.py
305
MultiServer.py
|
@ -11,6 +11,7 @@ import typing
|
|||
import inspect
|
||||
import weakref
|
||||
import datetime
|
||||
import threading
|
||||
|
||||
import ModuleUpdate
|
||||
|
||||
|
@ -33,6 +34,7 @@ CLIENT_PLAYING = 0
|
|||
CLIENT_GOAL = 1
|
||||
|
||||
|
||||
|
||||
class Client(Endpoint):
|
||||
version: typing.List[int] = [0, 0, 0]
|
||||
tags: typing.List[str] = []
|
||||
|
@ -46,9 +48,8 @@ class Client(Endpoint):
|
|||
self.send_index = 0
|
||||
self.tags = []
|
||||
self.version = [0, 0, 0]
|
||||
self.messageprocessor = ClientMessageProcessor(ctx, self)
|
||||
self.messageprocessor = client_message_processor(ctx, self)
|
||||
self.ctx = weakref.ref(ctx)
|
||||
ctx.client_connection_timers[self.team, self.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
@property
|
||||
def wants_item_notification(self):
|
||||
|
@ -57,8 +58,10 @@ class Client(Endpoint):
|
|||
|
||||
class Context(Node):
|
||||
def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int,
|
||||
item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled"):
|
||||
item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
|
||||
auto_shutdown: typing.SupportsFloat = 0):
|
||||
super(Context, self).__init__()
|
||||
self.shutdown_task = None
|
||||
self.data_filename = None
|
||||
self.save_filename = None
|
||||
self.saving = False
|
||||
|
@ -83,20 +86,27 @@ class Context(Node):
|
|||
self.item_cheat = item_cheat
|
||||
self.running = True
|
||||
self.client_activity_timers: typing.Dict[
|
||||
typing.Tuple[int, int], datetime.datetime] = {} # datatime of last new item check
|
||||
typing.Tuple[int, int], datetime.datetime] = {} # datetime of last new item check
|
||||
self.client_connection_timers: typing.Dict[
|
||||
typing.Tuple[int, int], datetime.datetime] = {} # datetime of last connection
|
||||
self.client_game_state: typing.Dict[typing.Tuple[int, int], int] = collections.defaultdict(int)
|
||||
self.er_hint_data: typing.Dict[int, typing.Dict[int, str]] = {}
|
||||
self.auto_shutdown = auto_shutdown
|
||||
self.commandprocessor = ServerCommandProcessor(self)
|
||||
self.embedded_blacklist = {"host", "port"}
|
||||
self.client_ids: typing.Dict[typing.Tuple[int, int], datetime.datetime] = {}
|
||||
self.auto_save_interval = 60 # in seconds
|
||||
self.auto_saver_thread = None
|
||||
self.save_dirty = False
|
||||
self.tags = ['Berserker']
|
||||
|
||||
def load(self, multidatapath: str):
|
||||
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
|
||||
with open(multidatapath, 'rb') as f:
|
||||
self._load(f)
|
||||
self._load(json.loads(zlib.decompress(f.read()).decode("utf-8-sig")),
|
||||
use_embedded_server_options)
|
||||
self.data_filename = multidatapath
|
||||
|
||||
def _load(self, fileobj):
|
||||
jsonobj = json.loads(zlib.decompress(fileobj.read()).decode("utf-8-sig"))
|
||||
def _load(self, jsonobj: dict, use_embedded_server_options: bool):
|
||||
for team, names in enumerate(jsonobj['names']):
|
||||
for player, name in enumerate(names, 1):
|
||||
self.player_names[(team, player)] = name
|
||||
|
@ -106,8 +116,44 @@ class Context(Node):
|
|||
if "er_hint_data" in jsonobj:
|
||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||
for player, loc_data in jsonobj["er_hint_data"].items()}
|
||||
if use_embedded_server_options:
|
||||
server_options = jsonobj.get("server_options", {})
|
||||
self._set_options(server_options)
|
||||
|
||||
def init_save(self, enabled: bool):
|
||||
def _set_options(self, server_options: dict):
|
||||
|
||||
sentinel = object()
|
||||
for key, value in server_options.items():
|
||||
if key not in self.embedded_blacklist:
|
||||
current = getattr(self, key, sentinel)
|
||||
if current is not sentinel:
|
||||
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
|
||||
setattr(self, key, value)
|
||||
self.item_cheat = not server_options.get("disable_item_cheat", True)
|
||||
|
||||
def save(self, now=False) -> bool:
|
||||
if self.saving:
|
||||
if now:
|
||||
self.save_dirty = False
|
||||
return self._save()
|
||||
|
||||
self.save_dirty = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _save(self) -> bool:
|
||||
try:
|
||||
jsonstr = json.dumps(self.get_save())
|
||||
with open(self.save_filename, "wb") as f:
|
||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def init_save(self, enabled: bool = True):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
if not self.save_filename:
|
||||
|
@ -121,6 +167,24 @@ class Context(Node):
|
|||
logging.error('No save data found, starting a new game')
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self._start_async_saving()
|
||||
|
||||
def _start_async_saving(self):
|
||||
if not self.auto_saver_thread:
|
||||
def save_regularly():
|
||||
import time
|
||||
while self.running:
|
||||
time.sleep(self.auto_save_interval)
|
||||
if self.save_dirty:
|
||||
logging.debug("Saving multisave via thread.")
|
||||
self.save_dirty = False
|
||||
self._save()
|
||||
|
||||
self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True)
|
||||
self.auto_saver_thread.start()
|
||||
|
||||
import atexit
|
||||
atexit.register(self._save) # make sure we save on exit too
|
||||
|
||||
def get_save(self) -> dict:
|
||||
d = {
|
||||
|
@ -130,7 +194,11 @@ class Context(Node):
|
|||
"hints": tuple((key, list(value)) for key, value in self.hints.items()),
|
||||
"location_checks": tuple((key, tuple(value)) for key, value in self.location_checks.items()),
|
||||
"name_aliases": tuple((key, value) for key, value in self.name_aliases.items()),
|
||||
"client_game_state": tuple((key, value) for key, value in self.client_game_state.items())
|
||||
"client_game_state": tuple((key, value) for key, value in self.client_game_state.items()),
|
||||
"client_activity_timers": tuple(
|
||||
(key, value.timestamp()) for key, value in self.client_activity_timers.items()),
|
||||
"client_connection_timers": tuple(
|
||||
(key, value.timestamp()) for key, value in self.client_connection_timers.items()),
|
||||
}
|
||||
return d
|
||||
|
||||
|
@ -161,7 +229,16 @@ class Context(Node):
|
|||
self.name_aliases.update({tuple(key): value for key, value in savedata["name_aliases"]})
|
||||
if "client_game_state" in savedata:
|
||||
self.client_game_state.update({tuple(key): value for key, value in savedata["client_game_state"]})
|
||||
if "client_activity_timers" in savedata:
|
||||
self.client_connection_timers.update(
|
||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||
in savedata["client_connection_timers"]})
|
||||
self.client_activity_timers.update(
|
||||
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
|
||||
in savedata["client_activity_timers"]})
|
||||
|
||||
self.location_checks.update({tuple(key): set(value) for key, value in savedata["location_checks"]})
|
||||
|
||||
logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items '
|
||||
f'for {len(received_items)} players')
|
||||
|
||||
|
@ -254,11 +331,11 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||
in ctx.endpoints if client.auth],
|
||||
# tags are for additional features in the communication.
|
||||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ['Berserker'],
|
||||
'tags': ctx.tags,
|
||||
'version': Utils._version_tuple,
|
||||
'forfeit_mode': ctx.forfeit_mode,
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'hint_cost' : ctx.hint_cost,
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points
|
||||
}]])
|
||||
|
||||
|
@ -274,7 +351,7 @@ async def on_client_joined(ctx: Context, client: Client):
|
|||
client.team + 1,
|
||||
".".join(str(x) for x in client.version),
|
||||
client.tags))
|
||||
|
||||
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
async def on_client_left(ctx: Context, client: Client):
|
||||
ctx.notify_all("%s (Team #%d) has left the game" % (ctx.get_aliased_name(client.team, client.slot), client.team + 1))
|
||||
|
@ -292,7 +369,7 @@ async def countdown(ctx: Context, timer):
|
|||
ctx.countdown_timer -= 1
|
||||
await asyncio.sleep(1)
|
||||
ctx.notify_all(f'[Server]: GO')
|
||||
|
||||
ctx.countdown_timer = 0
|
||||
|
||||
async def missing(ctx: Context, client: Client, locations: list):
|
||||
await ctx.send_msgs(client, [['Missing', {
|
||||
|
@ -386,7 +463,10 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations):
|
|||
send_new_items(ctx)
|
||||
|
||||
if found_items:
|
||||
save(ctx)
|
||||
for client in ctx.endpoints:
|
||||
if client.team == team and client.slot == slot:
|
||||
asyncio.create_task(ctx.send_msgs(client, [["HintPointUpdate", (get_client_points(ctx, client),)]]))
|
||||
ctx.save()
|
||||
|
||||
|
||||
def notify_team(ctx: Context, team: int, text: str):
|
||||
|
@ -394,15 +474,6 @@ def notify_team(ctx: Context, team: int, text: str):
|
|||
ctx.broadcast_team(team, [['Print', text]])
|
||||
|
||||
|
||||
def save(ctx: Context):
|
||||
if ctx.saving:
|
||||
try:
|
||||
jsonstr = json.dumps(ctx.get_save())
|
||||
with open(ctx.save_filename, "wb") as f:
|
||||
f.write(zlib.compress(jsonstr.encode("utf-8")))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[Utils.Hint]:
|
||||
hints = []
|
||||
|
@ -550,9 +621,33 @@ class CommandProcessor(metaclass=CommandMeta):
|
|||
self.output(str(exception))
|
||||
|
||||
|
||||
class CommonCommandProcessor(CommandProcessor):
|
||||
ctx: Context
|
||||
|
||||
simple_options = {"hint_cost": int,
|
||||
"location_check_points": int,
|
||||
"password": str,
|
||||
"forfeit_mode": str,
|
||||
"item_cheat": bool,
|
||||
"auto_save_interval": int}
|
||||
|
||||
def _cmd_countdown(self, seconds: str = "10") -> bool:
|
||||
"""Start a countdown in seconds"""
|
||||
try:
|
||||
timer = int(seconds, 10)
|
||||
except ValueError:
|
||||
timer = 10
|
||||
asyncio.create_task(countdown(self.ctx, timer))
|
||||
return True
|
||||
|
||||
def _cmd_options(self):
|
||||
"""List all current options. Warning: lists password."""
|
||||
self.output("Current options:")
|
||||
for option in self.simple_options:
|
||||
self.output(f"Option {option} is set to {getattr(self.ctx, option)}")
|
||||
|
||||
class ClientMessageProcessor(CommandProcessor):
|
||||
marker = "!"
|
||||
ctx: Context
|
||||
|
||||
def __init__(self, ctx: Context, client: Client):
|
||||
self.ctx = ctx
|
||||
|
@ -625,48 +720,48 @@ class ClientMessageProcessor(CommandProcessor):
|
|||
"Your client is too old to send game beaten information. Please update, load you savegame and reconnect.")
|
||||
return False
|
||||
|
||||
def _cmd_countdown(self, seconds: str = "10") -> bool:
|
||||
"""Start a countdown in seconds"""
|
||||
try:
|
||||
timer = int(seconds, 10)
|
||||
except ValueError:
|
||||
timer = 10
|
||||
asyncio.create_task(countdown(self.ctx, timer))
|
||||
return True
|
||||
|
||||
def _cmd_missing(self) -> bool:
|
||||
"""List all missing location checks from the server's perspective"""
|
||||
|
||||
locations = []
|
||||
for location_id, location_name in Regions.lookup_id_to_name.items(): # cheat console is -1, keep in mind
|
||||
if location_id != -1 and location_id not in self.ctx.location_checks[self.client.team, self.client.slot]:
|
||||
locations.append(location_name)
|
||||
|
||||
if len(locations) > 0:
|
||||
asyncio.create_task(missing(self.ctx, self.client, locations))
|
||||
if self.client.version < [2, 3, 0]:
|
||||
buffer = ""
|
||||
for location in locations:
|
||||
buffer += f'Missing: {location}\n'
|
||||
self.output(buffer + f"Found {len(locations)} missing location checks")
|
||||
else:
|
||||
asyncio.create_task(missing(self.ctx, self.client, locations))
|
||||
else:
|
||||
self.output("No missing location checks found.")
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_alias(self, alias_name: str = ""):
|
||||
"""Set your alias to the passed name."""
|
||||
if alias_name:
|
||||
alias_name = alias_name[:15].strip()
|
||||
alias_name = alias_name[:16].strip()
|
||||
self.ctx.name_aliases[self.client.team, self.client.slot] = alias_name
|
||||
self.output(f"Hello, {alias_name}")
|
||||
update_aliases(self.ctx, self.client.team)
|
||||
save(self.ctx)
|
||||
self.ctx.save()
|
||||
return True
|
||||
elif (self.client.team, self.client.slot) in self.ctx.name_aliases:
|
||||
del (self.ctx.name_aliases[self.client.team, self.client.slot])
|
||||
self.output("Removed Alias")
|
||||
update_aliases(self.ctx, self.client.team)
|
||||
save(self.ctx)
|
||||
self.ctx.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_getitem(self, item_name: str) -> bool:
|
||||
"""Cheat in an item"""
|
||||
"""Cheat in an item, if it is enabled on this server"""
|
||||
if self.ctx.item_cheat:
|
||||
item_name, usable, response = get_intended_text(item_name, Items.item_table.keys())
|
||||
if usable:
|
||||
|
@ -685,9 +780,7 @@ class ClientMessageProcessor(CommandProcessor):
|
|||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """
|
||||
points_available = self.ctx.location_check_points * len(
|
||||
self.ctx.location_checks[self.client.team, self.client.slot]) - \
|
||||
self.ctx.hint_cost * self.ctx.hints_used[self.client.team, self.client.slot]
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
if not item_or_location:
|
||||
self.output(f"A hint costs {self.ctx.hint_cost} points. "
|
||||
f"You have {points_available} points.")
|
||||
|
@ -746,10 +839,11 @@ class ClientMessageProcessor(CommandProcessor):
|
|||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||
|
||||
else:
|
||||
self.output(
|
||||
"Could not pay for everything. Rerun the hint later with more points to get the remaining hints.")
|
||||
if not_found_hints:
|
||||
self.output(
|
||||
"Could not pay for everything. Rerun the hint later with more points to get the remaining hints.")
|
||||
notify_hints(self.ctx, self.client.team, found_hints + hints)
|
||||
save(self.ctx)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
else:
|
||||
|
@ -767,6 +861,10 @@ class ClientMessageProcessor(CommandProcessor):
|
|||
return False
|
||||
|
||||
|
||||
def get_client_points(ctx: Context, client: Client) -> int:
|
||||
return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) -
|
||||
ctx.hint_cost * ctx.hints_used[client.team, client.slot])
|
||||
|
||||
async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
||||
if type(cmd) is not str:
|
||||
await ctx.send_msgs(client, [['InvalidCmd']])
|
||||
|
@ -787,8 +885,17 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
|||
errors.add('InvalidRom')
|
||||
else:
|
||||
team, slot = ctx.rom_names[tuple(args['rom'])]
|
||||
if any([c.slot == slot and c.team == team for c in ctx.endpoints if c.auth]):
|
||||
errors.add('SlotAlreadyTaken')
|
||||
# this can only ever be 0 or 1 elements
|
||||
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
|
||||
if clients:
|
||||
# likely same player with a "ghosted" slot. We bust the ghost.
|
||||
if "uuid" in args and ctx.client_ids[team, slot] == args["uuid"]:
|
||||
await clients[0].socket.close() # we have to await the DC of the ghost, so not to create data pasta
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
client.slot = slot
|
||||
else:
|
||||
errors.add('SlotAlreadyTaken')
|
||||
else:
|
||||
client.name = ctx.player_names[(team, slot)]
|
||||
client.team = team
|
||||
|
@ -797,6 +904,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
|||
if errors:
|
||||
await ctx.send_msgs(client, [['ConnectionRefused', list(errors)]])
|
||||
else:
|
||||
ctx.client_ids[client.team, client.slot] = args.get("uuid", None)
|
||||
client.auth = True
|
||||
client.version = args.get('version', Client.version)
|
||||
client.tags = args.get('tags', Client.tags)
|
||||
|
@ -868,14 +976,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd, args):
|
|||
client.messageprocessor(args)
|
||||
|
||||
|
||||
def set_password(ctx: Context, password):
|
||||
ctx.password = password
|
||||
logging.warning('Password set to ' + password if password else 'Password disabled')
|
||||
|
||||
|
||||
class ServerCommandProcessor(CommandProcessor):
|
||||
ctx: Context
|
||||
|
||||
class ServerCommandProcessor(CommonCommandProcessor):
|
||||
def __init__(self, ctx: Context):
|
||||
self.ctx = ctx
|
||||
super(ServerCommandProcessor, self).__init__()
|
||||
|
@ -897,9 +998,13 @@ class ServerCommandProcessor(CommandProcessor):
|
|||
|
||||
def _cmd_save(self) -> bool:
|
||||
"""Save current state to multidata"""
|
||||
save(self.ctx)
|
||||
self.output("Game saved")
|
||||
return True
|
||||
if self.ctx.saving:
|
||||
self.ctx.save(True)
|
||||
self.output("Game saved")
|
||||
return True
|
||||
else:
|
||||
self.output("Saving is disabled.")
|
||||
return False
|
||||
|
||||
def _cmd_players(self) -> bool:
|
||||
"""Get information about connected players"""
|
||||
|
@ -909,11 +1014,14 @@ class ServerCommandProcessor(CommandProcessor):
|
|||
def _cmd_exit(self) -> bool:
|
||||
"""Shutdown the server"""
|
||||
asyncio.create_task(self.ctx.server.ws_server._close())
|
||||
if self.ctx.shutdown_task:
|
||||
self.ctx.shutdown_task.cancel()
|
||||
self.ctx.running = False
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_alias(self, player_name_then_alias_name):
|
||||
"""Set a player's alias, by listing their base name and then their intended alias."""
|
||||
player_name, alias_name = player_name_then_alias_name.split(" ", 1)
|
||||
player_name, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
|
@ -924,24 +1032,18 @@ class ServerCommandProcessor(CommandProcessor):
|
|||
self.ctx.name_aliases[team, slot] = alias_name
|
||||
self.output(f"Named {player_name} as {alias_name}")
|
||||
update_aliases(self.ctx, team)
|
||||
save(self.ctx)
|
||||
self.ctx.save()
|
||||
return True
|
||||
else:
|
||||
del (self.ctx.name_aliases[team, slot])
|
||||
self.output(f"Removed Alias for {player_name}")
|
||||
update_aliases(self.ctx, team)
|
||||
save(self.ctx)
|
||||
self.ctx.save()
|
||||
return True
|
||||
else:
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_password(self, new_password: str = "") -> bool:
|
||||
"""Set the server password. Leave the password text empty to remove the password"""
|
||||
set_password(self.ctx, new_password if new_password else None)
|
||||
return True
|
||||
|
||||
@mark_raw
|
||||
def _cmd_forfeit(self, player_name: str) -> bool:
|
||||
"""Send out the remaining items from a player's game to their intended recipients"""
|
||||
|
@ -1002,6 +1104,25 @@ class ServerCommandProcessor(CommandProcessor):
|
|||
self.output(response)
|
||||
return False
|
||||
|
||||
def _cmd_option(self, option_name: str, option: str):
|
||||
"""Set options for the server. Warning: expires on restart"""
|
||||
|
||||
attrtype = self.simple_options.get(option_name, None)
|
||||
if attrtype:
|
||||
if attrtype == bool:
|
||||
def attrtype(input_text: str):
|
||||
if input_text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
setattr(self.ctx, option_name, attrtype(option))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.simple_options.items())
|
||||
self.output(f"Unrecognized Option {option_name}, known: "
|
||||
f"{', '.join(known)}")
|
||||
return False
|
||||
|
||||
async def console(ctx: Context):
|
||||
session = prompt_toolkit.PromptSession()
|
||||
|
@ -1045,15 +1166,46 @@ def parse_args() -> argparse.Namespace:
|
|||
disabled: !remaining is never available
|
||||
goal: !remaining can be used after goal completion
|
||||
''')
|
||||
parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int,
|
||||
help="automatically shut down the server after this many minutes without new location checks. "
|
||||
"0 to keep running. Not yet implemented.")
|
||||
parser.add_argument('--use_embedded_options', action="store_true",
|
||||
help='retrieve forfeit, remaining and hint options from the multidata file,'
|
||||
' instead of host.yaml')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
async def auto_shutdown(ctx, to_cancel=None):
|
||||
await asyncio.sleep(ctx.auto_shutdown * 60)
|
||||
while ctx.running:
|
||||
if not ctx.client_activity_timers.values():
|
||||
asyncio.create_task(ctx.server.ws_server._close())
|
||||
ctx.running = False
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
newest_activity = max(ctx.client_activity_timers.values())
|
||||
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
|
||||
seconds = ctx.auto_shutdown * 60 - delta.total_seconds()
|
||||
if seconds < 0:
|
||||
asyncio.create_task(ctx.server.ws_server._close())
|
||||
ctx.running = False
|
||||
if to_cancel:
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
logging.info("Shutting down due to inactivity.")
|
||||
else:
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
|
||||
|
||||
ctx = Context(args.host, args.port, args.password, args.location_check_points, args.hint_cost,
|
||||
not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode)
|
||||
not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode, args.auto_shutdown)
|
||||
|
||||
data_filename = args.multidata
|
||||
|
||||
|
@ -1063,9 +1215,9 @@ async def main(args: argparse.Namespace):
|
|||
import tkinter.filedialog
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*multidata"),))
|
||||
data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.multidata"),))
|
||||
|
||||
ctx.load(data_filename)
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception('Failed to read multiworld data (%s)' % e)
|
||||
|
@ -1078,9 +1230,20 @@ async def main(args: argparse.Namespace):
|
|||
ip = args.host if args.host else Utils.get_public_ipv4()
|
||||
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
|
||||
'No password' if not ctx.password else 'Password: %s' % ctx.password))
|
||||
|
||||
await ctx.server
|
||||
await console(ctx)
|
||||
console_task = asyncio.create_task(console(ctx))
|
||||
if ctx.auto_shutdown:
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
|
||||
await console_task
|
||||
if ctx.shutdown_task:
|
||||
await ctx.shutdown_task
|
||||
|
||||
|
||||
client_message_processor = ClientMessageProcessor
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main(parse_args()))
|
||||
try:
|
||||
asyncio.run(main(parse_args()))
|
||||
except asyncio.exceptions.CancelledError:
|
||||
pass
|
||||
|
|
90
Mystery.py
90
Mystery.py
|
@ -14,11 +14,11 @@ from Utils import parse_yaml
|
|||
from Rom import get_sprite_from_name
|
||||
from EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from Main import get_seed, seeddigits
|
||||
from Items import item_name_groups, item_table
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
def mystery_argparse():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
multiargs, _ = parser.parse_known_args()
|
||||
|
@ -46,15 +46,17 @@ def main():
|
|||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
if args.seed is None:
|
||||
random.seed(None)
|
||||
seed = random.randint(0, 999999999)
|
||||
else:
|
||||
seed = args.seed
|
||||
|
||||
def main(args=None):
|
||||
if not args:
|
||||
args = mystery_argparse()
|
||||
|
||||
seed = get_seed(args.seed)
|
||||
random.seed(seed)
|
||||
|
||||
seedname = "M"+(f"{random.randint(0, 999999999)}".zfill(9))
|
||||
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
|
||||
print(f"Generating mystery for {args.multi} player{'s' if args.multi > 1 else ''}, {seedname} Seed {seed}")
|
||||
|
||||
weights_cache = {}
|
||||
|
@ -63,14 +65,14 @@ def main():
|
|||
weights_cache[args.weights] = get_weights(args.weights)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
|
||||
print(f"Weights: {args.weights} >> {weights_cache[args.weights]['description']}")
|
||||
print(f"Weights: {args.weights} >> {get_choice('description', weights_cache[args.weights], 'No description specified')}")
|
||||
if args.meta:
|
||||
try:
|
||||
weights_cache[args.meta] = get_weights(args.meta)
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
|
||||
meta_weights = weights_cache[args.meta]
|
||||
print(f"Meta: {args.meta} >> {meta_weights['meta_description']}")
|
||||
print(f"Meta: {args.meta} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
|
||||
if args.samesettings:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
|
||||
|
@ -80,7 +82,7 @@ def main():
|
|||
try:
|
||||
if path not in weights_cache:
|
||||
weights_cache[path] = get_weights(path)
|
||||
print(f"P{player} Weights: {path} >> {weights_cache[path]['description']}")
|
||||
print(f"P{player} Weights: {path} >> {get_choice('description', weights_cache[path], 'No description specified')}")
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
|
||||
|
@ -223,15 +225,17 @@ def convert_to_on_off(value):
|
|||
return {True: "on", False: "off"}.get(value, value)
|
||||
|
||||
|
||||
def get_choice(option, root) -> typing.Any:
|
||||
def get_choice(option, root, value=None) -> typing.Any:
|
||||
if option not in root:
|
||||
return None
|
||||
return value
|
||||
if type(root[option]) is not dict:
|
||||
return interpret_on_off(root[option])
|
||||
if not root[option]:
|
||||
return None
|
||||
return interpret_on_off(
|
||||
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
|
||||
return value
|
||||
if any(root[option].values()):
|
||||
return interpret_on_off(
|
||||
random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0])
|
||||
raise RuntimeError(f"All options specified in {option} are weighted as zero.")
|
||||
|
||||
|
||||
def handle_name(name: str):
|
||||
|
@ -251,13 +255,13 @@ def roll_settings(weights):
|
|||
ret.name = handle_name(ret.name)
|
||||
|
||||
glitches_required = get_choice('glitches_required', weights)
|
||||
if glitches_required not in ['none', 'no_logic', 'overworld_glitches']:
|
||||
if glitches_required not in ['none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
|
||||
logging.warning("Only NMG, OWG and No Logic supported")
|
||||
glitches_required = 'none'
|
||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches'}[
|
||||
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
|
||||
'minor_glitches' : 'minorglitches'}[
|
||||
glitches_required]
|
||||
ret.progression_balancing = get_choice('progression_balancing',
|
||||
weights) if 'progression_balancing' in weights else True
|
||||
ret.progression_balancing = get_choice('progression_balancing', weights, True)
|
||||
# item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
|
@ -269,10 +273,10 @@ def roll_settings(weights):
|
|||
elif not dungeon_items:
|
||||
dungeon_items = ""
|
||||
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights) if 'map_shuffle' in weights else 'm' in dungeon_items
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights) if 'compass_shuffle' in weights else 'c' in dungeon_items
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights) if 'smallkey_shuffle' in weights else 's' in dungeon_items
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights) if 'bigkey_shuffle' in weights else 'b' in dungeon_items
|
||||
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
|
||||
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle', weights, 's' in dungeon_items)
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
|
||||
|
||||
ret.accessibility = get_choice('accessibility', weights)
|
||||
|
||||
|
@ -286,17 +290,21 @@ def roll_settings(weights):
|
|||
'pedestal': 'pedestal',
|
||||
'triforce_hunt': 'triforcehunt',
|
||||
'triforce-hunt': 'triforcehunt', # deprecated, moving all goals to `_`
|
||||
'local_triforce_hunt': 'localtriforcehunt'
|
||||
'local_triforce_hunt': 'localtriforcehunt',
|
||||
'ganon_triforce_hunt': 'ganontriforcehunt',
|
||||
'local_ganon_triforce_hunt': 'localganontriforcehunt'
|
||||
}[goal]
|
||||
ret.openpyramid = goal == 'fast_ganon'
|
||||
ret.openpyramid = goal in ['fast_ganon', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt']
|
||||
|
||||
ret.crystals_gt = get_choice('tower_open', weights)
|
||||
|
||||
ret.crystals_ganon = get_choice('ganon_open', weights)
|
||||
|
||||
ret.triforce_pieces_required = get_choice('triforce_pieces_required',
|
||||
weights) if "triforce_pieces_required" in weights else 20
|
||||
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 30)
|
||||
ret.triforce_pieces_available = get_choice('triforce_pieces_available', weights, 30)
|
||||
ret.triforce_pieces_available = min(max(1, int(ret.triforce_pieces_available)), 90)
|
||||
|
||||
ret.triforce_pieces_required = get_choice('triforce_pieces_required', weights, 20)
|
||||
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
|
||||
|
||||
ret.mode = get_choice('world_state', weights)
|
||||
if ret.mode == 'retro':
|
||||
|
@ -323,7 +331,9 @@ def roll_settings(weights):
|
|||
|
||||
ret.shuffleenemies = {'none': 'none',
|
||||
'shuffled': 'shuffled',
|
||||
'random': 'chaos'
|
||||
'random': 'chaos',
|
||||
'chaosthieves': 'chaosthieves',
|
||||
'chaos': 'chaos'
|
||||
}[get_choice('enemy_shuffle', weights)]
|
||||
|
||||
ret.enemy_damage = {'default': 'default',
|
||||
|
@ -335,7 +345,7 @@ def roll_settings(weights):
|
|||
|
||||
ret.shufflepots = get_choice('pot_shuffle', weights)
|
||||
|
||||
ret.beemizer = int(get_choice('beemizer', weights)) if 'beemizer' in weights else 0
|
||||
ret.beemizer = int(get_choice('beemizer', weights, 0))
|
||||
|
||||
ret.timer = {'none': False,
|
||||
None: False,
|
||||
|
@ -344,11 +354,11 @@ def roll_settings(weights):
|
|||
'timed_ohko': 'timed-ohko',
|
||||
'ohko': 'ohko',
|
||||
'timed_countdown': 'timed-countdown',
|
||||
'display': 'display'}[get_choice('timer', weights)] if 'timer' in weights.keys() else False
|
||||
'display': 'display'}[get_choice('timer', weights, False)]
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights) if 'dungeon_counters' in weights else 'default'
|
||||
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
|
||||
|
||||
ret.progressive = convert_to_on_off(get_choice('progressive', weights)) if "progressive" in weights else 'on'
|
||||
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
|
@ -362,18 +372,22 @@ def roll_settings(weights):
|
|||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights) if 'glitch_boots' in weights else True
|
||||
ret.glitch_boots = get_choice('glitch_boots', weights, True)
|
||||
|
||||
ret.remote_items = get_choice('remote_items', weights) if 'remote_items' in weights else False
|
||||
ret.remote_items = get_choice('remote_items', weights, False)
|
||||
|
||||
ret.local_items = set()
|
||||
if get_choice("local_keys", weights, "l" in dungeon_items):
|
||||
ret.local_items = item_name_groups["Small Keys"] | item_name_groups["Big Keys"]
|
||||
else:
|
||||
ret.local_items = set()
|
||||
for item_name in weights.get('local_items', []):
|
||||
items = item_name_groups.get(item_name, {item_name})
|
||||
for item in items:
|
||||
if item in item_table:
|
||||
ret.local_items.add(item)
|
||||
else:
|
||||
logging.warning(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
|
||||
|
||||
ret.local_items = ",".join(ret.local_items)
|
||||
|
||||
if 'rom' in weights:
|
||||
|
|
18
Patch.py
18
Patch.py
|
@ -21,6 +21,7 @@ def get_base_rom_path(file_name: str = "") -> str:
|
|||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
|
@ -38,7 +39,9 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||
|
||||
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
|
||||
patch = yaml.dump({"meta": metadata,
|
||||
"patch": patch})
|
||||
"patch": patch,
|
||||
"game": "alttp",
|
||||
"base_checksum": JAP10HASH})
|
||||
return patch.encode(encoding="utf-8-sig")
|
||||
|
||||
|
||||
|
@ -49,24 +52,27 @@ def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
|
|||
return generate_yaml(patch, metadata)
|
||||
|
||||
|
||||
def create_patch_file(rom_file_to_patch: str, server: str = "") -> str:
|
||||
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None) -> str:
|
||||
bytes = generate_patch(load_bytes(rom_file_to_patch),
|
||||
{
|
||||
"server": server}) # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
target = os.path.splitext(rom_file_to_patch)[0] + ".bmbp"
|
||||
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".bmbp"
|
||||
write_lzma(bytes, target)
|
||||
return target
|
||||
|
||||
|
||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||
def create_rom_bytes(patch_file: str) -> Tuple[dict, str, bytearray]:
|
||||
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
|
||||
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
|
||||
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
|
||||
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
|
||||
target = os.path.splitext(patch_file)[0] + ".sfc"
|
||||
return data["meta"], target, patched_data
|
||||
|
||||
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
|
||||
data, target, patched_data = create_rom_bytes(patch_file)
|
||||
with open(target, "wb") as f:
|
||||
f.write(patched_data)
|
||||
return data["meta"], target
|
||||
return data, target
|
||||
|
||||
|
||||
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
|
||||
|
|
236
Plando.py
236
Plando.py
|
@ -1,236 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
import sys
|
||||
|
||||
from BaseClasses import World
|
||||
from Regions import create_regions
|
||||
from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit
|
||||
from Rom import patch_rom, LocalRom, write_string_to_rom, apply_rom_settings, get_sprite_from_name
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Items import ItemFactory
|
||||
from ItemList import difficulties
|
||||
from Main import create_playthrough
|
||||
|
||||
__version__ = '0.2-dev'
|
||||
|
||||
def main(args):
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# initialize the world
|
||||
world = World(1, 'vanilla', 'noglitches', 'standard', 'normal', 'none', 'on', 'ganon', 'freshness', False, False, False, False, False, False, None, False)
|
||||
world.player_names[1].append("Player1")
|
||||
logger = logging.getLogger('')
|
||||
|
||||
hasher = hashlib.md5()
|
||||
with open(args.plando, 'rb') as plandofile:
|
||||
buf = plandofile.read()
|
||||
hasher.update(buf)
|
||||
world.seed = int(hasher.hexdigest(), 16) % 1000000000
|
||||
|
||||
random.seed(world.seed)
|
||||
|
||||
logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando)
|
||||
|
||||
world.difficulty_requirements[1] = difficulties[world.difficulty[1]]
|
||||
|
||||
create_regions(world, 1)
|
||||
create_dungeons(world, 1)
|
||||
|
||||
link_entrances(world, 1)
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
set_rules(world, 1)
|
||||
|
||||
logger.info('Fill the world.')
|
||||
|
||||
text_patches = []
|
||||
|
||||
fill_world(world, args.plando, text_patches)
|
||||
|
||||
if world.get_entrance('Dam', 1).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', 1).connected_region.name != 'Swamp Palace (Entrance)':
|
||||
world.swamp_patch_required[1] = True
|
||||
|
||||
logger.info('Calculating playthrough.')
|
||||
|
||||
try:
|
||||
create_playthrough(world)
|
||||
except RuntimeError:
|
||||
if args.ignore_unsolvable:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
logger.info('Patching ROM.')
|
||||
|
||||
rom = LocalRom(args.rom)
|
||||
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)
|
||||
|
||||
for textname, texttype, text in text_patches:
|
||||
if texttype == 'text':
|
||||
write_string_to_rom(rom, textname, text)
|
||||
#elif texttype == 'credit':
|
||||
# write_credits_string_to_rom(rom, textname, text)
|
||||
|
||||
outfilebase = 'Plando_%s_%s' % (os.path.splitext(os.path.basename(args.plando))[0], world.seed)
|
||||
|
||||
rom.write_to_file('%s.sfc' % outfilebase)
|
||||
if args.create_spoiler:
|
||||
world.spoiler.to_file('%s_Spoiler.txt' % outfilebase)
|
||||
|
||||
logger.info('Done. Enjoy.')
|
||||
logger.debug('Total Time: %s', time.perf_counter() - start_time)
|
||||
|
||||
return world
|
||||
|
||||
|
||||
def fill_world(world, plando, text_patches):
|
||||
mm_medallion = 'Ether'
|
||||
tr_medallion = 'Quake'
|
||||
logger = logging.getLogger('')
|
||||
with open(plando, 'r') as plandofile:
|
||||
for line in plandofile.readlines():
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
if ':' in line:
|
||||
line = line.lstrip()
|
||||
|
||||
if line.startswith('!'):
|
||||
if line.startswith('!mm_medallion'):
|
||||
_, medallionstr = line.split(':', 1)
|
||||
mm_medallion = medallionstr.strip()
|
||||
elif line.startswith('!tr_medallion'):
|
||||
_, medallionstr = line.split(':', 1)
|
||||
tr_medallion = medallionstr.strip()
|
||||
elif line.startswith('!mode'):
|
||||
_, modestr = line.split(':', 1)
|
||||
world.mode = {1: modestr.strip()}
|
||||
elif line.startswith('!logic'):
|
||||
_, logicstr = line.split(':', 1)
|
||||
world.logic = {1: logicstr.strip()}
|
||||
elif line.startswith('!goal'):
|
||||
_, goalstr = line.split(':', 1)
|
||||
world.goal = {1: goalstr.strip()}
|
||||
elif line.startswith('!light_cone_sewers'):
|
||||
_, sewerstr = line.split(':', 1)
|
||||
world.sewer_light_cone = {1: sewerstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!light_cone_lw'):
|
||||
_, lwconestr = line.split(':', 1)
|
||||
world.light_world_light_cone = lwconestr.strip().lower() == 'true'
|
||||
elif line.startswith('!light_cone_dw'):
|
||||
_, dwconestr = line.split(':', 1)
|
||||
world.dark_world_light_cone = dwconestr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_trock_doors'):
|
||||
_, trdstr = line.split(':', 1)
|
||||
world.fix_trock_doors = {1: trdstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_trock_exit'):
|
||||
_, trfstr = line.split(':', 1)
|
||||
world.fix_trock_exit = {1: trfstr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_gtower_exit'):
|
||||
_, gtfstr = line.split(':', 1)
|
||||
world.fix_gtower_exit = gtfstr.strip().lower() == 'true'
|
||||
elif line.startswith('!fix_pod_exit'):
|
||||
_, podestr = line.split(':', 1)
|
||||
world.fix_palaceofdarkness_exit = {1: podestr.strip().lower() == 'true'}
|
||||
elif line.startswith('!fix_skullwoods_exit'):
|
||||
_, swestr = line.split(':', 1)
|
||||
world.fix_skullwoods_exit = {1: swestr.strip().lower() == 'true'}
|
||||
elif line.startswith('!check_beatable_only'):
|
||||
_, chkbtstr = line.split(':', 1)
|
||||
world.check_beatable_only = chkbtstr.strip().lower() == 'true'
|
||||
elif line.startswith('!ganon_death_pyramid_respawn'):
|
||||
_, gnpstr = line.split(':', 1)
|
||||
world.ganon_at_pyramid = gnpstr.strip().lower() == 'true'
|
||||
elif line.startswith('!save_quit_boss'):
|
||||
_, sqbstr = line.split(':', 1)
|
||||
world.save_and_quite_from_boss = sqbstr.strip().lower() == 'true'
|
||||
elif line.startswith('!text_'):
|
||||
textname, text = line.split(':', 1)
|
||||
text_patches.append([textname.lstrip('!text_').strip(), 'text', text.strip()])
|
||||
#temporarilly removed. New credits system not ready to handle this.
|
||||
#elif line.startswith('!credits_'):
|
||||
# textname, text = line.split(':', 1)
|
||||
# text_patches.append([textname.lstrip('!credits_').strip(), 'credits', text.strip()])
|
||||
continue
|
||||
|
||||
locationstr, itemstr = line.split(':', 1)
|
||||
location = world.get_location(locationstr.strip(), 1)
|
||||
if location is None:
|
||||
logger.warning('Unknown location: %s', locationstr)
|
||||
continue
|
||||
else:
|
||||
item = ItemFactory(itemstr.strip(), 1)
|
||||
if item is not None:
|
||||
world.push_item(location, item)
|
||||
if item.smallkey or item.bigkey:
|
||||
location.event = True
|
||||
elif '<=>' in line:
|
||||
entrance, exit = line.split('<=>', 1)
|
||||
connect_two_way(world, entrance.strip(), exit.strip(), 1)
|
||||
elif '=>' in line:
|
||||
entrance, exit = line.split('=>', 1)
|
||||
connect_entrance(world, entrance.strip(), exit.strip(), 1)
|
||||
elif '<=' in line:
|
||||
entrance, exit = line.split('<=', 1)
|
||||
connect_exit(world, exit.strip(), entrance.strip(), 1)
|
||||
|
||||
world.required_medallions[1] = (mm_medallion, tr_medallion)
|
||||
|
||||
# set up Agahnim Events
|
||||
world.get_location('Agahnim 1', 1).event = True
|
||||
world.get_location('Agahnim 1', 1).item = ItemFactory('Beat Agahnim 1', 1)
|
||||
world.get_location('Agahnim 2', 1).event = True
|
||||
world.get_location('Agahnim 2', 1).item = ItemFactory('Beat Agahnim 2', 1)
|
||||
|
||||
|
||||
def start():
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
|
||||
parser.add_argument('--ignore_unsolvable', help='Do not abort if seed is deemed unsolvable.', action='store_true')
|
||||
parser.add_argument('--rom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||
parser.add_argument('--loglevel', default='info', const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
|
||||
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
|
||||
help='''\
|
||||
Select the rate at which the menu opens and closes.
|
||||
(default: %(default)s)
|
||||
''')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
|
||||
parser.add_argument('--heartbeep', default='normal', const='normal', nargs='?', choices=['normal', 'half', 'quarter', 'off'],
|
||||
help='Select the rate at which the heart beep sound is played at low health.')
|
||||
parser.add_argument('--heartcolor', default='red', const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow'],
|
||||
help='Select the color of Link\'s heart meter. (default: %(default)s)')
|
||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||
parser.add_argument('--sprite', help='Path to a sprite sheet to use for Link. Needs to be in binary format and have a length of 0x7000 (28672) bytes.')
|
||||
parser.add_argument('--plando', help='Filled out template to use for setting up the rom.')
|
||||
args = parser.parse_args()
|
||||
|
||||
# ToDo: Validate files further than mere existance
|
||||
if not os.path.isfile(args.rom):
|
||||
input('Could not find valid base rom for patching at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.rom)
|
||||
sys.exit(1)
|
||||
if not os.path.isfile(args.plando):
|
||||
input('Could not find Plandomizer distribution at expected path %s. Please run with -h to see help for further information. \nPress Enter to exit.' % args.plando)
|
||||
sys.exit(1)
|
||||
if args.sprite is not None and not os.path.isfile(args.sprite) and not get_sprite_from_name(args.sprite):
|
||||
input('Could not find link sprite sheet at given location. \nPress Enter to exit.')
|
||||
sys.exit(1)
|
||||
|
||||
# set up logger
|
||||
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[args.loglevel]
|
||||
logging.basicConfig(format='%(message)s', level=loglevel)
|
||||
|
||||
main(args=args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
start()
|
|
@ -1,244 +0,0 @@
|
|||
# Lines starting with a # are comments and ignored by the parsers
|
||||
# Lines without a : are also ignored
|
||||
|
||||
# These are special instructions for setting the medallion requirements to enter the dungeons
|
||||
!mm_medallion: Bombos
|
||||
!tr_medallion: Quake
|
||||
|
||||
# This sets the game mode
|
||||
!mode: open
|
||||
|
||||
# This sets the logic (for verification purposes)
|
||||
!logic: noglitches
|
||||
|
||||
# This sets the goal (only used for generating the spoiler log)
|
||||
!goal: ganon
|
||||
|
||||
# Now we fill in all locations
|
||||
|
||||
Mushroom: Mushroom
|
||||
Bottle Merchant: Bottle
|
||||
Flute Spot: Flute
|
||||
Sunken Treasure: Nothing
|
||||
Purple Chest: Nothing
|
||||
Blind's Hideout - Top: Nothing
|
||||
Blind's Hideout - Left: Nothing
|
||||
Blind's Hideout - Right: Nothing
|
||||
Blind's Hideout - Far Left: Nothing
|
||||
Blind's Hideout - Far Right: Nothing
|
||||
Link's Uncle: Fighter Sword
|
||||
Secret Passage: Nothing
|
||||
King Zora: Flippers
|
||||
Zora's Ledge: Nothing
|
||||
King's Tomb: Cape
|
||||
Floodgate Chest: Nothing
|
||||
Link's House: Lamp
|
||||
Kakariko Tavern: Nothing
|
||||
Chicken House: Nothing
|
||||
Aginah's Cave: Nothing
|
||||
Sahasrahla's Hut - Left: Nothing
|
||||
Sahasrahla's Hut - Middle: Nothing
|
||||
Sahasrahla's Hut - Right: Nothing
|
||||
Sahasrahla: Pegasus Boots
|
||||
Kakariko Well - Top: Nothing
|
||||
Kakariko Well - Left: Nothing
|
||||
Kakariko Well - Middle: Nothing
|
||||
Kakariko Well - Right: Nothing
|
||||
Kakariko Well - Bottom: Nothing
|
||||
Blacksmith: Tempered Sword
|
||||
Magic Bat: Magic Upgrade (1/2)
|
||||
Sick Kid: Bug Catching Net
|
||||
Hobo: Bottle
|
||||
Lost Woods Hideout: Nothing
|
||||
Lumberjack Tree: Nothing
|
||||
Cave 45: Nothing
|
||||
Graveyard Cave: Nothing
|
||||
Checkerboard Cave: Nothing
|
||||
Mini Moldorm Cave - Far Left: Nothing
|
||||
Mini Moldorm Cave - Left: Nothing
|
||||
Mini Moldorm Cave - Right: Nothing
|
||||
Mini Moldorm Cave - Far Right: Nothing
|
||||
Mini Moldorm Cave - Generous Guy: Nothing
|
||||
Ice Rod Cave: Ice Rod
|
||||
Bonk Rock Cave: Nothing
|
||||
Library: Book of Mudora
|
||||
Potion Shop: Magic Powder
|
||||
Lake Hylia Island: Nothing
|
||||
Maze Race: Nothing
|
||||
Desert Ledge: Nothing
|
||||
Desert Palace - Big Chest: Power Glove
|
||||
Desert Palace - Torch: Small Key (Desert Palace)
|
||||
Desert Palace - Map Chest: Nothing
|
||||
Desert Palace - Compass Chest: Nothing
|
||||
Desert Palace - Big Key Chest: Big Key (Desert Palace)
|
||||
Desert Palace - Boss: Nothing
|
||||
Desert Palace - Prize: Blue Pendant
|
||||
Eastern Palace - Compass Chest: Nothing
|
||||
Eastern Palace - Big Chest: Bow
|
||||
Eastern Palace - Cannonball Chest: Nothing
|
||||
Eastern Palace - Big Key Chest: Big Key (Eastern Palace)
|
||||
Eastern Palace - Map Chest: Nothing
|
||||
Eastern Palace - Boss: Nothing
|
||||
Eastern Palace - Prize: Green Pendant
|
||||
Master Sword Pedestal: Master Sword
|
||||
Hyrule Castle - Boomerang Chest: Nothing
|
||||
Hyrule Castle - Map Chest: Nothing
|
||||
Hyrule Castle - Zelda's Chest: Nothing
|
||||
Sewers - Dark Cross: Small Key (Escape)
|
||||
Sewers - Secret Room - Left: Nothing
|
||||
Sewers - Secret Room - Middle: Nothing
|
||||
Sewers - Secret Room - Right: Nothing
|
||||
Sanctuary: Sanctuary Heart Container
|
||||
Castle Tower - Room 03: Small Key (Agahnims Tower)
|
||||
Castle Tower - Dark Maze: Small Key (Agahnims Tower)
|
||||
Old Man: Magic Mirror
|
||||
Spectacle Rock Cave: Nothing
|
||||
Paradox Cave Lower - Far Left: Nothing
|
||||
Paradox Cave Lower - Left: Nothing
|
||||
Paradox Cave Lower - Right: Nothing
|
||||
Paradox Cave Lower - Far Right: Nothing
|
||||
Paradox Cave Lower - Middle: Nothing
|
||||
Paradox Cave Upper - Left: Nothing
|
||||
Paradox Cave Upper - Right: Nothing
|
||||
Spiral Cave: Nothing
|
||||
Ether Tablet: Ether
|
||||
Spectacle Rock: Nothing
|
||||
Tower of Hera - Basement Cage: Small Key (Tower of Hera)
|
||||
Tower of Hera - Map Chest: Nothing
|
||||
Tower of Hera - Big Key Chest: Big Key (Tower of Hera)
|
||||
Tower of Hera - Compass Chest: Nothing
|
||||
Tower of Hera - Big Chest: Moon Pearl
|
||||
Tower of Hera - Boss: Nothing
|
||||
Tower of Hera - Prize: Red Pendant
|
||||
Pyramid: Nothing
|
||||
Catfish: Quake
|
||||
Stumpy: Shovel
|
||||
Digging Game: Nothing
|
||||
Bombos Tablet: Bombos
|
||||
Hype Cave - Top: Nothing
|
||||
Hype Cave - Middle Right: Nothing
|
||||
Hype Cave - Middle Left: Nothing
|
||||
Hype Cave - Bottom: Nothing
|
||||
Hype Cave - Generous Guy: Nothing
|
||||
Peg Cave: Nothing
|
||||
Pyramid Fairy - Left: Golden Sword
|
||||
Pyramid Fairy - Right: Silver Arrows
|
||||
Brewery: Nothing
|
||||
C-Shaped House: Nothing
|
||||
Chest Game: Nothing
|
||||
Bumper Cave Ledge: Nothing
|
||||
Mire Shed - Left: Nothing
|
||||
Mire Shed - Right: Nothing
|
||||
Superbunny Cave - Top: Nothing
|
||||
Superbunny Cave - Bottom: Nothing
|
||||
Spike Cave: Cane of Byrna
|
||||
Hookshot Cave - Top Right: Nothing
|
||||
Hookshot Cave - Top Left: Nothing
|
||||
Hookshot Cave - Bottom Right: Nothing
|
||||
Hookshot Cave - Bottom Left: Nothing
|
||||
Floating Island: Nothing
|
||||
Mimic Cave: Nothing
|
||||
Swamp Palace - Entrance: Small Key (Swamp Palace)
|
||||
Swamp Palace - Map Chest: Nothing
|
||||
Swamp Palace - Big Chest: Hookshot
|
||||
Swamp Palace - Compass Chest: Nothing
|
||||
Swamp Palace - Big Key Chest: Big Key (Swamp Palace)
|
||||
Swamp Palace - West Chest: Nothing
|
||||
Swamp Palace - Flooded Room - Left: Nothing
|
||||
Swamp Palace - Flooded Room - Right: Nothing
|
||||
Swamp Palace - Waterfall Room: Nothing
|
||||
Swamp Palace - Boss: Nothing
|
||||
Swamp Palace - Prize: Crystal 2
|
||||
Thieves' Town - Big Key Chest: Big Key (Thieves Town)
|
||||
Thieves' Town - Map Chest: Nothing
|
||||
Thieves' Town - Compass Chest: Nothing
|
||||
Thieves' Town - Ambush Chest: Nothing
|
||||
Thieves' Town - Attic: Nothing
|
||||
Thieves' Town - Big Chest: Titans Mitts
|
||||
Thieves' Town - Blind's Cell: Small Key (Thieves Town)
|
||||
Thieves' Town - Boss: Nothing
|
||||
Thieves' Town - Prize: Crystal 4
|
||||
Skull Woods - Compass Chest: Nothing
|
||||
Skull Woods - Map Chest: Nothing
|
||||
Skull Woods - Big Chest: Fire Rod
|
||||
Skull Woods - Pot Prison: Small Key (Skull Woods)
|
||||
Skull Woods - Pinball Room: Small Key (Skull Woods)
|
||||
Skull Woods - Big Key Chest: Big Key (Skull Woods)
|
||||
Skull Woods - Bridge Room: Small Key (Skull Woods)
|
||||
Skull Woods - Boss: Nothing
|
||||
Skull Woods - Prize: Crystal 3
|
||||
Ice Palace - Compass Chest: Nothing
|
||||
Ice Palace - Freezor Chest: Nothing
|
||||
Ice Palace - Big Chest: Blue Mail
|
||||
Ice Palace - Iced T Room: Small Key (Ice Palace)
|
||||
Ice Palace - Spike Room: Small Key (Ice Palace)
|
||||
Ice Palace - Big Key Chest: Big Key (Ice Palace)
|
||||
Ice Palace - Map Chest: Nothing
|
||||
Ice Palace - Boss: Nothing
|
||||
Ice Palace - Prize: Crystal 5
|
||||
Misery Mire - Big Chest: Cane of Somaria
|
||||
Misery Mire - Map Chest: Nothing
|
||||
Misery Mire - Main Lobby: Small Key (Misery Mire)
|
||||
Misery Mire - Bridge Chest: Small Key (Misery Mire)
|
||||
Misery Mire - Spike Chest: Small Key (Misery Mire)
|
||||
Misery Mire - Compass Chest: Nothing
|
||||
Misery Mire - Big Key Chest: Big Key (Misery Mire)
|
||||
Misery Mire - Boss: Nothing
|
||||
Misery Mire - Prize: Crystal 6
|
||||
Turtle Rock - Compass Chest: Nothing
|
||||
Turtle Rock - Roller Room - Left: Nothing
|
||||
Turtle Rock - Roller Room - Right: Small Key (Turtle Rock)
|
||||
Turtle Rock - Chain Chomps: Small Key (Turtle Rock)
|
||||
Turtle Rock - Big Key Chest: Big Key (Turtle Rock)
|
||||
Turtle Rock - Big Chest: Mirror Shield
|
||||
Turtle Rock - Crystaroller Room: Small Key (Turtle Rock)
|
||||
Turtle Rock - Eye Bridge - Bottom Left: Small Key (Turtle Rock)
|
||||
Turtle Rock - Eye Bridge - Bottom Right: Nothing
|
||||
Turtle Rock - Eye Bridge - Top Left: Nothing
|
||||
Turtle Rock - Eye Bridge - Top Right: Nothing
|
||||
Turtle Rock - Boss: Nothing
|
||||
Turtle Rock - Prize: Crystal 7
|
||||
Palace of Darkness - Shooter Room: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - The Arena - Bridge: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - Stalfos Basement: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - Big Key Chest: Big Key (Palace of Darkness)
|
||||
Palace of Darkness - The Arena - Ledge: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - Map Chest: Nothing
|
||||
Palace of Darkness - Compass Chest: Nothing
|
||||
# logic cannot account for hammer and small key in maze
|
||||
Palace of Darkness - Dark Basement - Left: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - Dark Basement - Right: Small Key (Palace of Darkness)
|
||||
Palace of Darkness - Dark Maze - Top: Nothing
|
||||
Palace of Darkness - Dark Maze - Bottom: Nothing
|
||||
Palace of Darkness - Big Chest: Hammer
|
||||
Palace of Darkness - Harmless Hellway: Nothing
|
||||
Palace of Darkness - Boss: Nothing
|
||||
Palace of Darkness - Prize: Crystal 1
|
||||
Ganons Tower - Bob's Torch: Small Key (Ganons Tower)
|
||||
Ganons Tower - Hope Room - Left: Nothing
|
||||
Ganons Tower - Hope Room - Right: Nothing
|
||||
Ganons Tower - Tile Room: Small Key (Ganons Tower)
|
||||
Ganons Tower - Compass Room - Top Left: Nothing
|
||||
Ganons Tower - Compass Room - Top Right: Nothing
|
||||
Ganons Tower - Compass Room - Bottom Left: Nothing
|
||||
Ganons Tower - Compass Room - Bottom Right: Nothing
|
||||
Ganons Tower - DMs Room - Top Left: Nothing
|
||||
Ganons Tower - DMs Room - Top Right: Nothing
|
||||
Ganons Tower - DMs Room - Bottom Left: Nothing
|
||||
Ganons Tower - DMs Room - Bottom Right: Nothing
|
||||
Ganons Tower - Map Chest: Nothing
|
||||
Ganons Tower - Firesnake Room: Small Key (Ganons Tower)
|
||||
Ganons Tower - Randomizer Room - Top Left: Nothing
|
||||
Ganons Tower - Randomizer Room - Top Right: Nothing
|
||||
Ganons Tower - Randomizer Room - Bottom Left: Nothing
|
||||
Ganons Tower - Randomizer Room - Bottom Right: Nothing
|
||||
Ganons Tower - Bob's Chest: Nothing
|
||||
Ganons Tower - Big Chest: Red Mail
|
||||
Ganons Tower - Big Key Room - Left: Nothing
|
||||
Ganons Tower - Big Key Room - Right: Nothing
|
||||
Ganons Tower - Big Key Chest: Big Key (Ganons Tower)
|
||||
Ganons Tower - Mini Helmasaur Room - Left: Nothing
|
||||
Ganons Tower - Mini Helmasaur Room - Right: Nothing
|
||||
Ganons Tower - Pre-Moldorm Chest: Small Key (Ganons Tower)
|
||||
Ganons Tower - Validation Chest: Nothing
|
||||
Ganon: Triforce
|
21
README.md
21
README.md
|
@ -1,33 +1,35 @@
|
|||
Berserker's Multiworld
|
||||
======================
|
||||
|
||||
This is a complete fork of Bonta's Multiworld V31.
|
||||
It is a drop-in replacement with everything from Bonta's Multiworld included.
|
||||
You can find a guide here: https://docs.google.com/document/d/1r7qs1-MK7YbFf2d-mEUeTy2wHykIf1ALG9pLtVvUbSw/edit#
|
||||
Or use the Wiki button at the top
|
||||
A Multiworld implementation for the Legend of Zelda: A Link to the Past Randomizer
|
||||
For setup and instructions there's a [Wiki](https://github.com/Berserker66/MultiWorld-Utilities/wiki)
|
||||
Downloads can be found at [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases), including compiled windows binaries.
|
||||
|
||||
Additions/Changes
|
||||
Additions/Changes compared to Bonta's V31
|
||||
-----------------
|
||||
|
||||
Project
|
||||
* Available in precompiled form and guided setup for Windows 64Bit on [Releases](https://github.com/Berserker66/MultiWorld-Utilities/releases) page.
|
||||
* Compatible with Python 3.7 and 3.8. Potentially future versions as well.
|
||||
* Compatible with Python 3.7 and 3.8. Forward Checks for Python 4.0 are done.
|
||||
* Update modules if they are too old, preventing a crash when trying to connect among potential other issues
|
||||
* Autoinstall missing modules
|
||||
* Allow newer versions of modules than specified, as they will *usually* not break compatibility
|
||||
* Support for V31 extendedmsu
|
||||
* Uses "V32" MSU
|
||||
* Has support for binary patching to allow legal distribution of multiworld rom files
|
||||
* Various performance improvements (over 100% faster in most cases)
|
||||
* Various fixes
|
||||
* Overworld Glitches Logic
|
||||
* Newer Entrance Randomizer Logic, allowing more potential item and boss locations
|
||||
* completely redesigned command interface, with `!help` and `/help`
|
||||
* New Goal: local triforce hunt - limits triforce pieces to your own world so it is your own goal to accomplish
|
||||
|
||||
MultiMystery.py
|
||||
* Allows you to generate a Multiworld with individual player mystery weights. Since weights can also be set to 100%, this also allows for individual settings for each player in a regular multiworld.
|
||||
Basis is a .yaml file that sets these weights. You can find an [easy.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/easy.yaml) in this project folder to get started.
|
||||
* Additional instructions are at the start of the file. Open with a text editor.
|
||||
* Configuration options in the host.yaml file.
|
||||
* Allows a new Mode called "Meta-Mystery", allowing certain mystery settings to apply to all players.
|
||||
* For example, everyone gets the same but random goal.
|
||||
|
||||
MultiServer.py
|
||||
* Supports automatic port-forwarding, can be enabled in host.yaml
|
||||
|
@ -35,6 +37,7 @@ Basis is a .yaml file that sets these weights. You can find an [easy.yaml](https
|
|||
* /forfeit Playername now works when the player is not currently connected
|
||||
* Added `/hint` and `!hint`, configuration in host.yaml and description in help
|
||||
* various commands, like /send and /hint use "fuzzy text matching", no longer requiring you to enter a location, player name or item name perfectly
|
||||
* Some item groups also exist, so `/hint Bottles` lists all bottle varieties
|
||||
|
||||
Mystery.py
|
||||
* Defaults to generating a non-race ROM (Bonta's only makes race ROMs at this time)
|
||||
|
@ -47,8 +50,12 @@ If a race ROM is desired, pass --create-race as argument to it
|
|||
* Option for "glitch_boots", allowing to run glitched modes without automatic boots
|
||||
* Supports new Meta-Mystery mode. Read [meta.yaml](https://github.com/Berserker66/MultiWorld-Utilities/blob/master/meta.yaml) for details.
|
||||
* Added `dungeonssimple` and `dungeonsfull` ER modes
|
||||
* Option for local items
|
||||
* Option for linked options
|
||||
* Added 'l' to dungeon_items to have a local-world keysanity
|
||||
|
||||
MultiClient.py
|
||||
* Has a Webbrowser based UI now
|
||||
* Awaits a QUsb2Snes connection when started, latching on when available
|
||||
* completely redesigned command interface, with `!help` and `/help`
|
||||
* Running it with a patch file will patch out the multiworld rom and then automatically connect to the host that created the multiworld
|
||||
|
|
112
Regions.py
112
Regions.py
|
@ -1,4 +1,5 @@
|
|||
import collections
|
||||
|
||||
from BaseClasses import Region, Location, Entrance, RegionType, Shop, ShopType
|
||||
|
||||
|
||||
|
@ -629,11 +630,116 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
|||
'Skull Woods - Prize': (
|
||||
[0x120A3, 0x53F12, 0x53F13, 0x180058, 0x18007B, 0xC704], None, True, 'Skull Woods'),
|
||||
'Ice Palace - Prize': (
|
||||
[0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
[0x120A4, 0x53F5A, 0x53F5B, 0x180059, 0x180073, 0xC705], None, True, 'Ice Palace'),
|
||||
'Misery Mire - Prize': (
|
||||
[0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
[0x120A2, 0x53F48, 0x53F49, 0x180057, 0x180075, 0xC703], None, True, 'Misery Mire'),
|
||||
'Turtle Rock - Prize': (
|
||||
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
[0x120A7, 0x53F24, 0x53F25, 0x18005C, 0x180079, 0xC708], None, True, 'Turtle Rock')}
|
||||
|
||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||
lookup_id_to_name[-1] = "cheat console"
|
||||
|
||||
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',
|
||||
1573194: 'Kings Grave Inner Rocks', 1573189: 'Kings Grave Inner Rocks',
|
||||
212328: 'Kings Grave Inner Rocks', 60175: 'Blinds Hideout',
|
||||
60178: 'Blinds Hideout', 60181: 'Blinds Hideout', 60184: 'Blinds Hideout',
|
||||
60187: 'Blinds Hideout', 188229: 'Hyrule Castle Secret Entrance Drop',
|
||||
59761: 'Hyrule Castle Secret Entrance Drop', 975299: 'Zoras River',
|
||||
1573193: 'Zoras River', 59824: 'Waterfall of Wishing',
|
||||
59857: 'Waterfall of Wishing', 59770: 'Kings Grave', 59788: 'Dam',
|
||||
59836: 'Links House', 59854: 'Tavern North', 59881: 'Chicken House',
|
||||
59890: 'Aginahs Cave', 60034: 'Sahasrahlas Hut', 60037: 'Sahasrahlas Hut',
|
||||
60040: 'Sahasrahlas Hut', 193020: 'Sahasrahlas Hut',
|
||||
60046: 'Kakariko Well Drop', 60049: 'Kakariko Well Drop',
|
||||
60052: 'Kakariko Well Drop', 60055: 'Kakariko Well Drop',
|
||||
60058: 'Kakariko Well Drop', 1572906: 'Blacksmiths Hut',
|
||||
1572885: 'Bat Cave Drop', 211407: 'Sick Kids House',
|
||||
212605: 'Hobo Bridge', 1572864: 'Lost Woods Hideout Drop',
|
||||
1572865: 'Lumberjack Tree Tree', 1572867: 'Cave 45',
|
||||
1572868: 'Graveyard Cave', 1572869: 'Checkerboard Cave',
|
||||
60226: 'Mini Moldorm Cave', 60229: 'Mini Moldorm Cave',
|
||||
60232: 'Mini Moldorm Cave', 60235: 'Mini Moldorm Cave',
|
||||
1572880: 'Mini Moldorm Cave', 60238: 'Ice Rod Cave',
|
||||
60223: 'Bonk Rock Cave', 1572882: 'Library', 1572884: 'Potion Shop',
|
||||
1573188: 'Lake Hylia Island Mirror Spot',
|
||||
1573186: 'Maze Race Mirror Spot', 1573187: 'Desert Ledge Return Rocks',
|
||||
59791: 'Desert Palace Entrance (West)',
|
||||
1573216: 'Desert Palace Entrance (West)',
|
||||
59830: 'Desert Palace Entrance (West)',
|
||||
59851: 'Desert Palace Entrance (West)',
|
||||
59842: 'Desert Palace Entrance (West)',
|
||||
1573201: 'Desert Palace Entrance (North)', 59767: 'Eastern Palace',
|
||||
59773: 'Eastern Palace', 59827: 'Eastern Palace', 59833: 'Eastern Palace',
|
||||
59893: 'Eastern Palace', 1573200: 'Eastern Palace',
|
||||
166320: 'Master Sword Meadow', 59764: 'Hyrule Castle Entrance (South)',
|
||||
60172: 'Hyrule Castle Entrance (South)',
|
||||
60169: 'Hyrule Castle Entrance (South)',
|
||||
59758: 'Hyrule Castle Entrance (South)',
|
||||
60253: 'Hyrule Castle Entrance (South)',
|
||||
60256: 'Hyrule Castle Entrance (South)',
|
||||
60259: 'Hyrule Castle Entrance (South)', 60025: 'Sanctuary S&Q',
|
||||
60085: 'Agahnims Tower', 60082: 'Agahnims Tower',
|
||||
1010170: 'Old Man Cave (West)', 1572866: 'Spectacle Rock Cave',
|
||||
60202: 'Paradox Cave (Bottom)', 60205: 'Paradox Cave (Bottom)',
|
||||
60208: 'Paradox Cave (Bottom)', 60211: 'Paradox Cave (Bottom)',
|
||||
60214: 'Paradox Cave (Bottom)', 60217: 'Paradox Cave (Bottom)',
|
||||
60220: 'Paradox Cave (Bottom)', 59839: 'Spiral Cave',
|
||||
1572886: 'Death Mountain (Top)', 1573184: 'Spectacle Rock Mirror Spot',
|
||||
1573218: 'Tower of Hera', 59821: 'Tower of Hera', 59878: 'Tower of Hera',
|
||||
59899: 'Tower of Hera', 59896: 'Tower of Hera', 1573202: 'Tower of Hera',
|
||||
1573191: 'Top of Pyramid', 975237: 'Catfish Entrance Rock',
|
||||
209095: 'South Dark World Bridge', 1573192: 'South Dark World Bridge',
|
||||
1572887: 'Bombos Tablet Mirror Spot', 60190: 'Hype Cave',
|
||||
60193: 'Hype Cave', 60196: 'Hype Cave', 60199: 'Hype Cave',
|
||||
1572881: 'Hype Cave', 1572870: 'Dark World Hammer Peg Cave',
|
||||
59776: 'Pyramid Fairy', 59779: 'Pyramid Fairy', 59884: 'Brewery',
|
||||
59887: 'C-Shaped House', 60840: 'Chest Game',
|
||||
1573190: 'Bumper Cave (Bottom)', 60019: 'Mire Shed', 60022: 'Mire Shed',
|
||||
60028: 'Superbunny Cave (Top)', 60031: 'Superbunny Cave (Top)',
|
||||
60043: 'Spike Cave', 60241: 'Hookshot Cave', 60244: 'Hookshot Cave',
|
||||
60250: 'Hookshot Cave', 60247: 'Hookshot Cave',
|
||||
1573185: 'Floating Island Mirror Spot', 59845: 'Mimic Cave',
|
||||
60061: 'Swamp Palace', 59782: 'Swamp Palace', 59785: 'Swamp Palace',
|
||||
60064: 'Swamp Palace', 60070: 'Swamp Palace', 60067: 'Swamp Palace',
|
||||
60073: 'Swamp Palace', 60076: 'Swamp Palace', 60079: 'Swamp Palace',
|
||||
1573204: 'Swamp Palace', 59908: 'Thieves Town', 59905: 'Thieves Town',
|
||||
59911: 'Thieves Town', 59914: 'Thieves Town', 59917: 'Thieves Town',
|
||||
59920: 'Thieves Town', 59923: 'Thieves Town', 1573206: 'Thieves Town',
|
||||
59803: 'Skull Woods First Section Door',
|
||||
59848: 'Skull Woods First Section Hole (East)',
|
||||
59794: 'Skull Woods First Section Hole (West)',
|
||||
59809: 'Skull Woods First Section Hole (West)',
|
||||
59800: 'Skull Woods First Section Hole (North)',
|
||||
59806: 'Skull Woods Second Section Door (East)',
|
||||
59902: 'Skull Woods Final Section', 1573205: 'Skull Woods Final Section',
|
||||
59860: 'Ice Palace', 59797: 'Ice Palace', 59818: 'Ice Palace',
|
||||
59875: 'Ice Palace', 59872: 'Ice Palace', 59812: 'Ice Palace',
|
||||
59869: 'Ice Palace', 1573207: 'Ice Palace', 60007: 'Misery Mire',
|
||||
60010: 'Misery Mire', 59998: 'Misery Mire', 60001: 'Misery Mire',
|
||||
59866: 'Misery Mire', 60004: 'Misery Mire', 60013: 'Misery Mire',
|
||||
1573208: 'Misery Mire', 59938: 'Turtle Rock', 59932: 'Turtle Rock',
|
||||
59935: 'Turtle Rock', 59926: 'Turtle Rock',
|
||||
59941: 'Dark Death Mountain Ledge (West)',
|
||||
59929: 'Dark Death Mountain Ledge (East)',
|
||||
59956: 'Dark Death Mountain Ledge (West)',
|
||||
59953: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59950: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59947: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59944: 'Turtle Rock Isolated Ledge Entrance',
|
||||
1573209: 'Turtle Rock Isolated Ledge Entrance',
|
||||
59995: 'Palace of Darkness', 59965: 'Palace of Darkness',
|
||||
59977: 'Palace of Darkness', 59959: 'Palace of Darkness',
|
||||
59962: 'Palace of Darkness', 59986: 'Palace of Darkness',
|
||||
59971: 'Palace of Darkness', 59980: 'Palace of Darkness',
|
||||
59983: 'Palace of Darkness', 59989: 'Palace of Darkness',
|
||||
59992: 'Palace of Darkness', 59968: 'Palace of Darkness',
|
||||
59974: 'Palace of Darkness', 1573203: 'Palace of Darkness',
|
||||
1573217: 'Ganons Tower', 60121: 'Ganons Tower', 60124: 'Ganons Tower',
|
||||
60130: 'Ganons Tower', 60133: 'Ganons Tower', 60136: 'Ganons Tower',
|
||||
60139: 'Ganons Tower', 60142: 'Ganons Tower', 60088: 'Ganons Tower',
|
||||
60091: 'Ganons Tower', 60094: 'Ganons Tower', 60097: 'Ganons Tower',
|
||||
60115: 'Ganons Tower', 60112: 'Ganons Tower', 60100: 'Ganons Tower',
|
||||
60103: 'Ganons Tower', 60106: 'Ganons Tower', 60109: 'Ganons Tower',
|
||||
60127: 'Ganons Tower', 60118: 'Ganons Tower', 60148: 'Ganons Tower',
|
||||
60151: 'Ganons Tower', 60145: 'Ganons Tower', 60157: 'Ganons Tower',
|
||||
60160: 'Ganons Tower', 60163: 'Ganons Tower', 60166: 'Ganons Tower'}
|
||||
|
|
203
Rom.py
203
Rom.py
|
@ -22,54 +22,7 @@ from EntranceShuffle import door_addresses
|
|||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = 'aec17dd8b3c76c16d0b0311c36eb1c00'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
|
||||
def __init__(self, name=None, hash=None):
|
||||
self.name = name
|
||||
self.hash = hash
|
||||
self.orig_buffer = None
|
||||
self.patches = {}
|
||||
self.addresses = []
|
||||
|
||||
def write_byte(self, address, value):
|
||||
self.write_bytes(address, [value])
|
||||
|
||||
def write_bytes(self, startaddress, values):
|
||||
if not values:
|
||||
return
|
||||
values = list(values)
|
||||
|
||||
pos = bisect.bisect_right(self.addresses, startaddress)
|
||||
intervalstart = self.addresses[pos-1] if pos else None
|
||||
intervalpatch = self.patches[str(intervalstart)] if pos else None
|
||||
|
||||
if pos and startaddress <= intervalstart + len(intervalpatch): # merge with previous segment
|
||||
offset = startaddress - intervalstart
|
||||
intervalpatch[offset:offset+len(values)] = values
|
||||
startaddress = intervalstart
|
||||
values = intervalpatch
|
||||
else: # new segment
|
||||
self.addresses.insert(pos, startaddress)
|
||||
self.patches[str(startaddress)] = values
|
||||
pos = pos + 1
|
||||
|
||||
while pos < len(self.addresses) and self.addresses[pos] <= startaddress + len(values): # merge the next segment into this one
|
||||
intervalstart = self.addresses[pos]
|
||||
values.extend(self.patches[str(intervalstart)][startaddress+len(values)-intervalstart:])
|
||||
del self.patches[str(intervalstart)]
|
||||
del self.addresses[pos]
|
||||
|
||||
def write_to_file(self, file):
|
||||
with open(file, 'w') as stream:
|
||||
json.dump([self.patches], stream)
|
||||
|
||||
def get_hash(self):
|
||||
h = hashlib.md5()
|
||||
h.update(json.dumps([self.patches]).encode('utf-8'))
|
||||
return h.hexdigest()
|
||||
RANDOMIZERBASEHASH = 'a567da86e8bd499256da4bba2209a3fd'
|
||||
|
||||
class LocalRom(object):
|
||||
|
||||
|
@ -93,6 +46,10 @@ class LocalRom(object):
|
|||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.buffer)
|
||||
|
||||
def read_from_file(self, file):
|
||||
with open(file, 'rb') as stream:
|
||||
self.buffer = bytearray(stream.read())
|
||||
|
||||
@staticmethod
|
||||
def fromJsonRom(rom, file, rom_size=0x200000):
|
||||
ret = LocalRom(file, True, rom.name, rom.hash)
|
||||
|
@ -101,12 +58,37 @@ class LocalRom(object):
|
|||
ret.write_bytes(int(address), values)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def verify(buffer, expected=RANDOMIZERBASEHASH):
|
||||
buffermd5 = hashlib.md5()
|
||||
buffermd5.update(buffer)
|
||||
return expected == buffermd5.hexdigest()
|
||||
|
||||
def patch_base_rom(self):
|
||||
from Patch import create_patch_file, create_rom_bytes
|
||||
|
||||
if os.path.isfile(local_path('basepatch.sfc')):
|
||||
with open(local_path('basepatch.sfc'), 'rb') as stream:
|
||||
buffer = bytearray(stream.read())
|
||||
|
||||
if self.verify(buffer):
|
||||
self.buffer = buffer
|
||||
if not os.path.exists(local_path(os.path.join('data', 'basepatch.bmbp'))):
|
||||
create_patch_file(local_path('basepatch.sfc'))
|
||||
return
|
||||
|
||||
if os.path.isfile(local_path(os.path.join('data', 'basepatch.bmbp'))):
|
||||
_, target, buffer = create_rom_bytes(local_path(os.path.join('data', 'basepatch.bmbp')))
|
||||
if self.verify(buffer):
|
||||
self.buffer = bytearray(buffer)
|
||||
with open(local_path('basepatch.sfc'), 'wb') as stream:
|
||||
stream.write(buffer)
|
||||
return
|
||||
|
||||
# verify correct checksum of baserom
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(self.buffer)
|
||||
if JAP10HASH != basemd5.hexdigest():
|
||||
logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
|
||||
if not self.verify(self.buffer, JAP10HASH):
|
||||
logging.getLogger('').warning(
|
||||
'Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.')
|
||||
|
||||
# extend to 2MB
|
||||
self.buffer.extend(bytearray([0x00]) * (0x200000 - len(self.buffer)))
|
||||
|
@ -120,10 +102,14 @@ class LocalRom(object):
|
|||
self.write_bytes(int(baseaddress), values)
|
||||
|
||||
# verify md5
|
||||
patchedmd5 = hashlib.md5()
|
||||
patchedmd5.update(self.buffer)
|
||||
if patchedmd5.hexdigest() not in [RANDOMIZERBASEHASH]:
|
||||
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.')
|
||||
if self.verify(self.buffer):
|
||||
with open(local_path('basepatch.sfc'), 'wb') as stream:
|
||||
stream.write(self.buffer)
|
||||
create_patch_file(local_path('basepatch.sfc'), destination=local_path(os.path.join('data', 'basepatch.bmbp')))
|
||||
os.remove(local_path('data/base2current.json'))
|
||||
else:
|
||||
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 write_crc(self):
|
||||
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
|
@ -157,19 +143,16 @@ def read_rom(stream) -> bytearray:
|
|||
buffer = buffer[0x200:]
|
||||
return buffer
|
||||
|
||||
def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, random_sprite_on_hit):
|
||||
baserom_path = os.path.abspath(baserom_path)
|
||||
basepatch_path = os.path.abspath(local_path('data/base2current.json'))
|
||||
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
|
||||
randopatch_path = os.path.abspath(output_path(f'enemizer_randopatch_{player}.json'))
|
||||
def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, random_sprite_on_hit):
|
||||
randopatch_path = os.path.abspath(output_path(f'enemizer_randopatch_{player}.sfc'))
|
||||
options_path = os.path.abspath(output_path(f'enemizer_options_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(output_path(f'enemizer_output_{player}.json'))
|
||||
enemizer_output_path = os.path.abspath(output_path(f'enemizer_output_{player}.sfc'))
|
||||
|
||||
# write options file for enemizer
|
||||
options = {
|
||||
'RandomizeEnemies': world.enemy_shuffle[player] != 'none',
|
||||
'RandomizeEnemiesType': 3,
|
||||
'RandomizeBushEnemyChance': world.enemy_shuffle[player] == 'chaos',
|
||||
'RandomizeBushEnemyChance': 'chaos' in world.enemy_shuffle[player],
|
||||
'RandomizeEnemyHealthRange': world.enemy_health[player] != 'default',
|
||||
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
||||
world.enemy_health[player]],
|
||||
|
@ -178,12 +161,14 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
|
|||
'AllowEnemyZeroDamage': True,
|
||||
'ShuffleEnemyDamageGroups': world.enemy_damage[player] != 'default',
|
||||
'EnemyDamageChaosMode': world.enemy_damage[player] == 'chaos',
|
||||
'EasyModeEscape': False,
|
||||
'EasyModeEscape': world.mode[player] == "standard",
|
||||
'EnemiesAbsorbable': False,
|
||||
'AbsorbableSpawnRate': 10,
|
||||
'AbsorbableTypes': {
|
||||
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True, 'Key': True,
|
||||
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True, 'RedRupee': True
|
||||
'FullMagic': True, 'SmallMagic': True, 'Bomb_1': True, 'BlueRupee': True, 'Heart': True, 'BigKey': True,
|
||||
'Key': True,
|
||||
'Fairy': True, 'Arrow_10': True, 'Arrow_5': True, 'Bomb_8': True, 'Bomb_4': True, 'GreenRupee': True,
|
||||
'RedRupee': True
|
||||
},
|
||||
'BossMadness': False,
|
||||
'RandomizeBosses': True,
|
||||
|
@ -205,7 +190,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
|
|||
'GrayscaleMode': False,
|
||||
'GenerateSpoilers': False,
|
||||
'RandomizeLinkSpritePalette': False,
|
||||
'RandomizePots': shufflepots,
|
||||
'RandomizePots': world.shufflepots[player],
|
||||
'ShuffleMusic': False,
|
||||
'BootlegMagic': True,
|
||||
'CustomBosses': False,
|
||||
|
@ -218,7 +203,8 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
|
|||
'BeesLevel': 0,
|
||||
'RandomizeTileTrapPattern': world.enemy_shuffle[player] == 'chaos',
|
||||
'RandomizeTileTrapFloorTile': False,
|
||||
'AllowKillableThief': bool(random.randint(0,1)) if world.enemy_shuffle[player] == 'chaos' else world.enemy_shuffle[player] != 'none',
|
||||
'AllowKillableThief': bool(random.randint(0, 1)) if 'thieves' in world.enemy_shuffle[player] else
|
||||
world.enemy_shuffle[player] != 'none',
|
||||
'RandomizeSpriteOnHit': random_sprite_on_hit,
|
||||
'DebugMode': False,
|
||||
'DebugForceEnemy': False,
|
||||
|
@ -255,21 +241,14 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
|
|||
json.dump(options, f)
|
||||
|
||||
subprocess.check_call([os.path.abspath(enemizercli),
|
||||
'--rom', baserom_path,
|
||||
'--rom', randopatch_path,
|
||||
'--seed', str(world.rom_seeds[player]),
|
||||
'--base', basepatch_path,
|
||||
'--randomizer', randopatch_path,
|
||||
'--binary',
|
||||
'--enemizer', options_path,
|
||||
'--output', enemizer_output_path],
|
||||
cwd=os.path.dirname(enemizercli), stdout=subprocess.DEVNULL)
|
||||
|
||||
with open(enemizer_basepatch_path, 'r') as f:
|
||||
for patch in json.load(f):
|
||||
rom.write_bytes(patch["address"], patch["patchData"])
|
||||
|
||||
with open(enemizer_output_path, 'r') as f:
|
||||
for patch in json.load(f):
|
||||
rom.write_bytes(patch["address"], patch["patchData"])
|
||||
cwd=os.path.dirname(enemizercli))
|
||||
rom.read_from_file(enemizer_output_path)
|
||||
os.remove(enemizer_output_path)
|
||||
|
||||
if random_sprite_on_hit:
|
||||
_populate_sprite_table()
|
||||
|
@ -285,7 +264,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, shufflepots, r
|
|||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
for used in (randopatch_path, options_path, enemizer_output_path):
|
||||
for used in (randopatch_path, options_path):
|
||||
try:
|
||||
os.remove(used)
|
||||
except OSError:
|
||||
|
@ -942,6 +921,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
equip[0x38E] |= 0x80
|
||||
if startingstate.has('Silver Arrows', player):
|
||||
equip[0x38E] |= 0x40
|
||||
#TODO add Silver Bow
|
||||
|
||||
if startingstate.has('Titans Mitts', player):
|
||||
equip[0x354] = 2
|
||||
|
@ -980,7 +960,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
if item.player != player:
|
||||
continue
|
||||
|
||||
if item.name in ['Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
if item.name in ['Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
'Golden Sword', 'Tempered Sword', 'Master Sword', 'Fighter Sword', 'Progressive Sword',
|
||||
'Mirror Shield', 'Red Shield', 'Blue Shield', 'Progressive Shield',
|
||||
|
@ -992,22 +972,35 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
'Cape': (0x352, 1), 'Lamp': (0x34A, 1), 'Moon Pearl': (0x357, 1), 'Cane of Somaria': (0x350, 1), 'Cane of Byrna': (0x351, 1),
|
||||
'Fire Rod': (0x345, 1), 'Ice Rod': (0x346, 1), 'Bombos': (0x347, 1), 'Ether': (0x348, 1), 'Quake': (0x349, 1)}
|
||||
or_table = {'Green Pendant': (0x374, 0x04), 'Red Pendant': (0x374, 0x01), 'Blue Pendant': (0x374, 0x02),
|
||||
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40), 'Crystal 4': (0x37A, 0x20),
|
||||
'Crystal 1': (0x37A, 0x02), 'Crystal 2': (0x37A, 0x10), 'Crystal 3': (0x37A, 0x40),
|
||||
'Crystal 4': (0x37A, 0x20),
|
||||
'Crystal 5': (0x37A, 0x04), 'Crystal 6': (0x37A, 0x01), 'Crystal 7': (0x37A, 0x08),
|
||||
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20), 'Map (Eastern Palace)': (0x369, 0x20),
|
||||
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10), 'Map (Desert Palace)': (0x369, 0x10),
|
||||
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20), 'Map (Tower of Hera)': (0x368, 0x20),
|
||||
'Big Key (Escape)': (0x367, 0xC0), 'Compass (Escape)': (0x365, 0xC0), 'Map (Escape)': (0x369, 0xC0),
|
||||
'Big Key (Eastern Palace)': (0x367, 0x20), 'Compass (Eastern Palace)': (0x365, 0x20),
|
||||
'Map (Eastern Palace)': (0x369, 0x20),
|
||||
'Big Key (Desert Palace)': (0x367, 0x10), 'Compass (Desert Palace)': (0x365, 0x10),
|
||||
'Map (Desert Palace)': (0x369, 0x10),
|
||||
'Big Key (Tower of Hera)': (0x366, 0x20), 'Compass (Tower of Hera)': (0x364, 0x20),
|
||||
'Map (Tower of Hera)': (0x368, 0x20),
|
||||
'Big Key (Hyrule Castle)': (0x367, 0xC0), 'Compass (Hyrule Castle)': (0x365, 0xC0),
|
||||
'Map (Hyrule Castle)': (0x369, 0xC0),
|
||||
# doors-specific items
|
||||
'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08), 'Map (Agahnims Tower)': (0x369, 0x08),
|
||||
'Big Key (Agahnims Tower)': (0x367, 0x08), 'Compass (Agahnims Tower)': (0x365, 0x08),
|
||||
'Map (Agahnims Tower)': (0x369, 0x08),
|
||||
# end of doors-specific items
|
||||
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02), 'Map (Palace of Darkness)': (0x369, 0x02),
|
||||
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10), 'Map (Thieves Town)': (0x368, 0x10),
|
||||
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80), 'Map (Skull Woods)': (0x368, 0x80),
|
||||
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04), 'Map (Swamp Palace)': (0x369, 0x04),
|
||||
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40), 'Map (Ice Palace)': (0x368, 0x40),
|
||||
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
|
||||
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
|
||||
'Big Key (Palace of Darkness)': (0x367, 0x02), 'Compass (Palace of Darkness)': (0x365, 0x02),
|
||||
'Map (Palace of Darkness)': (0x369, 0x02),
|
||||
'Big Key (Thieves Town)': (0x366, 0x10), 'Compass (Thieves Town)': (0x364, 0x10),
|
||||
'Map (Thieves Town)': (0x368, 0x10),
|
||||
'Big Key (Skull Woods)': (0x366, 0x80), 'Compass (Skull Woods)': (0x364, 0x80),
|
||||
'Map (Skull Woods)': (0x368, 0x80),
|
||||
'Big Key (Swamp Palace)': (0x367, 0x04), 'Compass (Swamp Palace)': (0x365, 0x04),
|
||||
'Map (Swamp Palace)': (0x369, 0x04),
|
||||
'Big Key (Ice Palace)': (0x366, 0x40), 'Compass (Ice Palace)': (0x364, 0x40),
|
||||
'Map (Ice Palace)': (0x368, 0x40),
|
||||
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01),
|
||||
'Map (Misery Mire)': (0x369, 0x01),
|
||||
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08),
|
||||
'Map (Turtle Rock)': (0x368, 0x08),
|
||||
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
|
||||
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
|
||||
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Flute': (0x34C, 3, 0x38C, 0x01),
|
||||
|
@ -1021,7 +1014,7 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
'Small Key (Ice Palace)': [0x385],
|
||||
'Small Key (Misery Mire)': [0x383], 'Small Key (Turtle Rock)': [0x388],
|
||||
'Small Key (Ganons Tower)': [0x389],
|
||||
'Small Key (Universal)': [0x38B], 'Small Key (Escape)': [0x37C, 0x37D]}
|
||||
'Small Key (Universal)': [0x38B], 'Small Key (Hyrule Castle)': [0x37C, 0x37D]}
|
||||
bottles = {'Bottle': 2, 'Bottle (Red Potion)': 3, 'Bottle (Green Potion)': 4, 'Bottle (Blue Potion)': 5,
|
||||
'Bottle (Fairy)': 6, 'Bottle (Bee)': 7, 'Bottle (Good Bee)': 8}
|
||||
rupees = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)': 50, 'Rupees (100)': 100, 'Rupees (300)': 300}
|
||||
|
@ -1095,6 +1088,8 @@ def patch_rom(world, rom, player, team, enemized):
|
|||
|
||||
if world.goal[player] in ['pedestal', 'triforcehunt', 'localtriforcehunt']:
|
||||
rom.write_byte(0x18003E, 0x01) # make ganon invincible
|
||||
elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
|
||||
rom.write_byte(0x18003E, 0x05) # make ganon invincible until 20 triforce pieces are collected
|
||||
elif world.goal[player] in ['dungeons']:
|
||||
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
|
||||
elif world.goal[player] in ['crystals']:
|
||||
|
@ -1732,7 +1727,7 @@ def write_strings(rom, world, player, team):
|
|||
# We still need the older hints of course. Those are done here.
|
||||
|
||||
|
||||
silverarrows = world.find_items('Silver Arrows', player)
|
||||
silverarrows = world.find_items('Silver Bow', player)
|
||||
random.shuffle(silverarrows)
|
||||
silverarrow_hint = (' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!'
|
||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||
|
@ -1775,9 +1770,11 @@ def write_strings(rom, world, player, team):
|
|||
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
tt[
|
||||
'murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % \
|
||||
if world.goal[player] == 'triforcehunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||
else:
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % \
|
||||
world.treasure_hunt_count[player]
|
||||
elif world.goal[player] in ['pedestal']:
|
||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||
|
@ -1787,6 +1784,10 @@ def write_strings(rom, world, player, team):
|
|||
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
if world.goal[player] == 'ganontriforcehunt' and world.players > 1:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces with your friends to defeat Ganon.' % world.treasure_hunt_count[player]
|
||||
elif world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
|
||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces to defeat Ganon.' % world.treasure_hunt_count[player]
|
||||
|
||||
tt['kakariko_tavern_fisherman'] = TavernMan_texts[random.randint(0, len(TavernMan_texts) - 1)]
|
||||
|
||||
|
@ -2290,7 +2291,7 @@ RelevantItems = ['Bow',
|
|||
]
|
||||
|
||||
SmallKeys = ['Small Key (Eastern Palace)',
|
||||
'Small Key (Escape)',
|
||||
'Small Key (Hyrule Castle)',
|
||||
'Small Key (Desert Palace)',
|
||||
'Small Key (Tower of Hera)',
|
||||
'Small Key (Agahnims Tower)',
|
||||
|
|
112
Rules.py
112
Rules.py
|
@ -2,21 +2,25 @@ import collections
|
|||
import logging
|
||||
import OverworldGlitchRules
|
||||
from BaseClasses import RegionType, World, Entrance
|
||||
from Items import ItemFactory
|
||||
from Items import ItemFactory, progression_items, item_name_groups
|
||||
from OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
|
||||
|
||||
|
||||
def set_rules(world, player):
|
||||
|
||||
locality_rules(world, player)
|
||||
if world.logic[player] == 'nologic':
|
||||
logging.getLogger('').info('WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||
logging.getLogger('').info(
|
||||
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||
no_logic_rules(world, player)
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
exit.hide_path = True
|
||||
return
|
||||
|
||||
crossover_logic(world, player)
|
||||
|
||||
global_rules(world, player)
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
default_rules(world, player)
|
||||
|
||||
|
@ -39,7 +43,8 @@ def set_rules(world, player):
|
|||
fake_flipper_rules(world, player)
|
||||
overworld_glitches_rules(world, player)
|
||||
elif world.logic[player] == 'minorglitches':
|
||||
logging.getLogger('').info('Minor Glitches may be buggy still. No guarantee for proper logic checks.')
|
||||
no_glitches_rules(world, player)
|
||||
fake_flipper_rules(world, player)
|
||||
else:
|
||||
raise NotImplementedError('Not implemented yet')
|
||||
|
||||
|
@ -115,36 +120,63 @@ def add_lamp_requirement(spot, player):
|
|||
add_rule(spot, lambda state: state.has('Lamp', player, state.world.lamps_needed_for_dark_rooms))
|
||||
|
||||
|
||||
def forbid_item(location, item, player):
|
||||
def forbid_item(location, item, player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items(location, items: set, player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
|
||||
|
||||
|
||||
def add_item_rule(location, rule):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||
|
||||
|
||||
def item_in_locations(state, item, player, locations):
|
||||
for location in locations:
|
||||
if item_name(state, location[0], location[1]) == (item, player):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def item_name(state, location, player):
|
||||
location = state.world.get_location(location, player)
|
||||
if location.item is None:
|
||||
return None
|
||||
return (location.item.name, location.item.player)
|
||||
|
||||
def global_rules(world, player):
|
||||
# ganon can only carry triforce
|
||||
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
if world.goal[player] == "localtriforcehunt":
|
||||
|
||||
def locality_rules(world, player):
|
||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||
world.local_items[player].add('Triforce Piece')
|
||||
if world.local_items[player]:
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
for item in world.local_items[player]:
|
||||
forbid_item(location, item, player)
|
||||
forbid_items(location, world.local_items[player], player)
|
||||
|
||||
|
||||
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
|
||||
"Small Key (Universal)"}
|
||||
|
||||
|
||||
def crossover_logic(world, player):
|
||||
""" Simple and not graceful solution to logic loops if you mix no logic and logic.
|
||||
Making it so that logical progression cannot be placed in no logic worlds."""
|
||||
no_logic_players = set()
|
||||
for other_player in world.player_ids:
|
||||
if world.logic[other_player] == 'nologic':
|
||||
no_logic_players.add(other_player)
|
||||
if no_logic_players:
|
||||
for location in world.get_locations():
|
||||
if location.player in no_logic_players:
|
||||
forbid_items(location, non_crossover_items, player)
|
||||
|
||||
def global_rules(world, player):
|
||||
# ganon can only carry triforce
|
||||
add_item_rule(world.get_location('Ganon', player), lambda item: item.name == 'Triforce' and item.player == player)
|
||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
|
@ -171,26 +203,40 @@ def global_rules(world, player):
|
|||
set_rule(world.get_location('Spike Cave', player), lambda state:
|
||||
state.has('Hammer', player) and state.can_lift_rocks(player) and
|
||||
((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or
|
||||
(state.has('Cane of Byrna', player) and
|
||||
(state.can_extend_magic(player, 12, True) or
|
||||
(state.world.can_take_damage[player] and (state.has_Boots(player) or state.has_hearts(player, 4))))))
|
||||
)
|
||||
(state.has('Cane of Byrna', player) and
|
||||
(state.can_extend_magic(player, 12, True) or
|
||||
(state.world.can_take_damage[player] and (state.has_Boots(player) or state.has_hearts(player, 4))))))
|
||||
)
|
||||
|
||||
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player), lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
|
||||
lambda state: state.has('Hookshot', player) or state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Sewers Door', player), lambda state: state.has_key('Small Key (Escape)', player) or (world.retro[player] and world.mode[player] == 'standard')) # standard retro cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Sewers Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (world.retro[player] and world.mode[
|
||||
player] == 'standard')) # standard retro cannot access the shop
|
||||
set_rule(world.get_entrance('Sewers Back Door', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player),
|
||||
lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player))
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player), lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)', player))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player),
|
||||
lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)',
|
||||
player))
|
||||
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player), lambda state: state.has('Big Key (Eastern Palace)', player))
|
||||
set_rule(world.get_location('Eastern Palace - Boss', player), lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)', player) and state.world.get_location('Eastern Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Eastern Palace - Prize', player), lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)', player) and state.world.get_location('Eastern Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player),
|
||||
lambda state: state.has('Big Key (Eastern Palace)', player))
|
||||
set_rule(world.get_location('Eastern Palace - Boss', player),
|
||||
lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)',
|
||||
player) and state.world.get_location(
|
||||
'Eastern Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Eastern Palace - Prize', player),
|
||||
lambda state: state.can_shoot_arrows(player) and state.has('Big Key (Eastern Palace)',
|
||||
player) and state.world.get_location(
|
||||
'Eastern Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
for location in ['Eastern Palace - Boss', 'Eastern Palace - Big Chest']:
|
||||
forbid_item(world.get_location(location, player), 'Big Key (Eastern Palace)', player)
|
||||
|
||||
|
@ -377,8 +423,13 @@ def global_rules(world, player):
|
|||
'Ganons Tower - Pre-Moldorm Chest', 'Ganons Tower - Validation Chest']:
|
||||
forbid_item(world.get_location(location, player), 'Big Key (Ganons Tower)', player)
|
||||
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)
|
||||
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
|
||||
|
||||
if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_triforce_pieces(world.treasure_hunt_count[player], player)
|
||||
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
|
||||
else:
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)
|
||||
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
||||
|
@ -764,8 +815,10 @@ def add_conditional_lamps(world, player):
|
|||
|
||||
def open_rules(world, player):
|
||||
# softlock protection as you can reach the sewers small key door with a guard drop key
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.has_key('Small Key (Escape)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
|
||||
|
||||
|
||||
def swordless_rules(world, player):
|
||||
|
@ -774,7 +827,10 @@ def swordless_rules(world, player):
|
|||
set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Arrows', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player))
|
||||
if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Bow', player) and state.can_shoot_arrows(player) and state.has_triforce_pieces(world.treasure_hunt_count[player], player))
|
||||
else:
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has('Hammer', player) and state.has_fire_source(player) and state.has('Silver Bow', player) and state.can_shoot_arrows(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player))
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
|
|
93
Text.py
93
Text.py
|
@ -45,18 +45,18 @@ Uncle_texts = [
|
|||
"Don't worry.\nI got this\ncovered.",
|
||||
"Race you to\nthe castle!",
|
||||
"\n hi",
|
||||
"I'M JUST GOING\nOUT FOR A\nPACK OF SMOKES",
|
||||
"I'm just going\nout for a\npack of smokes",
|
||||
"It's dangerous\nto go alone.\nSee ya!",
|
||||
"ARE YOU A BAD\nENOUGH DUDE TO\nRESCUE ZELDA?",
|
||||
"Are you a bad\nenough dude to\nrescue Zelda?",
|
||||
"\n\n I AM ERROR",
|
||||
"This seed is\nsub 2 hours,\nguaranteed.",
|
||||
"The chest is\na secret to\neverybody.",
|
||||
"I'm off to\nfind the\nwind fish.",
|
||||
"The shortcut\nto Ganon\nis this way!",
|
||||
"THE MOON IS\nCRASHING! RUN\nFOR YOUR LIFE!",
|
||||
"The moon is\ncrashing! Run\nfor your life!",
|
||||
"Time to fight\nhe who must\nnot be named.",
|
||||
"RED MAIL\nIS FOR\nCOWARDS.",
|
||||
"HEY!\n\nLISTEN!",
|
||||
"Red Mail\nis for\ncowards.",
|
||||
"Hey!\n\nListen!",
|
||||
"Well\nexcuuuuuse me,\nprincess!",
|
||||
"5,000 Rupee\nreward for >\nYou're boned.",
|
||||
"Welcome to\nStoops Lonk's\nHoose",
|
||||
|
@ -66,6 +66,22 @@ Uncle_texts = [
|
|||
"Get to the\nchop...\ncastle!",
|
||||
"Come with me\nif you want\nto live",
|
||||
"I must go\nmy planet\nneeds me",
|
||||
"Are we in\ngo mode yet?",
|
||||
"Darn, I\nthought this\nwas combo.",
|
||||
"Don't check\nanything I\nwouldn't!",
|
||||
"I know where\nthe bow is!",
|
||||
"This message\nwill self\ndestruct.",
|
||||
"Time to cast\nMeteo on\nGanon!",
|
||||
"I have a\nlong, full\nlife ahead!",
|
||||
"Why did that\nsoda have a\nskull on it?",
|
||||
"Something\nrandom just\ncame up.",
|
||||
"I'm bad at\nthis. Can you\ndo it for me?",
|
||||
"Link!\n Wake up!\n ... Bye!",
|
||||
"Text me when\nyou hit\ngo mode.",
|
||||
"Turn off the\nstove before\nyou leave.",
|
||||
"It's raining.\nI'm taking\nthe umbrella.",
|
||||
"Count to 30.\nThen come\nfind me.",
|
||||
"Gonna shuffle\nall the items\nreal quick.",
|
||||
]
|
||||
Triforce_texts = [
|
||||
'Product has Hole in center. Bad seller, 0 out of 5.',
|
||||
|
@ -78,28 +94,28 @@ Triforce_texts = [
|
|||
"\n G G",
|
||||
"All your base\nare belong\nto us.",
|
||||
"You have ended\nthe domination\nof Dr. Wily",
|
||||
" thanks for\n playing!!!",
|
||||
"\n You Win!",
|
||||
" Thank you!\n your quest\n is over.",
|
||||
" A winner\n is\n you!",
|
||||
" Thanks for\n playing!!!",
|
||||
"\n You Win!",
|
||||
" Thank you!\n Your quest\n is over.",
|
||||
" A winner\n is\n you!",
|
||||
"\n WINNER!!",
|
||||
"\n I'm sorry\n\n but your\nprincess is in\nanother castle",
|
||||
"\n success!",
|
||||
" Whelp…\n that just\n happened",
|
||||
" Oh hey…\n it's you",
|
||||
"\n I'm sorry\n\n but your\nprincess is in\nanother castle",
|
||||
"\n Success!",
|
||||
" Whelp…\n that just\n happened",
|
||||
" Oh hey…\n it's you",
|
||||
"\n Wheeeeee!!",
|
||||
" Time for\n another one?",
|
||||
" Time for\n another one?",
|
||||
"and\n\n scene",
|
||||
"\n GOT EM!!",
|
||||
"\nTHE VALUUUE!!!",
|
||||
"\n Got 'em!!",
|
||||
"\nThe valuuue!!!",
|
||||
"Cool seed,\n\nright?",
|
||||
"\n We did it!",
|
||||
" Spam those\n emotes in\n wilds chat",
|
||||
" Spam those\n emotes in\n Wild's chat",
|
||||
"\n O M G",
|
||||
" Hello. Will\n you be my\n friend?",
|
||||
" Beetorp\n was\n here!",
|
||||
" Hello. Will\n you be my\n friend?",
|
||||
" Beetorp\n was\n here!",
|
||||
"The Wind Fish\nwill wake\nsoon. Hoot!",
|
||||
"meow meow meow\nmeow meow meow\n oh my god!",
|
||||
"Meow meow meow\nMeow meow meow\n Oh my god!",
|
||||
"Ahhhhhhhhh\nYa ya yaaaah\nYa ya yaaah",
|
||||
".done\n\n.comment lol",
|
||||
"You get to\ndrink from\nthe firehose",
|
||||
|
@ -107,7 +123,12 @@ Triforce_texts = [
|
|||
"You get one\nwish. Choose\nwisely, hero!",
|
||||
"Can you please\nbreak us three\nup? Thanks.",
|
||||
" Pick us up\n before we\n get dizzy!",
|
||||
"\n Honk."
|
||||
"Thank you,\nMikey. You’re\n2 minutes late",
|
||||
"This was a\n7000 series\ntrain.",
|
||||
" I'd buy\n that for\n a rupee!",
|
||||
" Did you like\n that bow\n placement?",
|
||||
"I promise the\nnext seed will\nbe better.",
|
||||
"\n Honk.",
|
||||
]
|
||||
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomest!']
|
||||
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'This text won\'t change.', 'Have you met my brother, Hasarahshla?']
|
||||
|
@ -146,6 +167,18 @@ Blind_texts = [
|
|||
"I tried to\ncatch fog,\nbut I mist.",
|
||||
"Winter is a\ngreat time\nto chill.",
|
||||
"Do you think\nthe Ice Rod\nis cool?",
|
||||
"Pyramids?\nI never saw\nthe point.",
|
||||
"Stone golems\nare created as\nblank slates.",
|
||||
"Desert humor\nis often dry.",
|
||||
"Ganon is a\nbacon of\ndespair!",
|
||||
"Butchering\ncows means\nhigh steaks.",
|
||||
"I can't search\nthe web...\nToo many links",
|
||||
"I can whistle\nMost pitches\nbut I can't C",
|
||||
"The Blinds\nStore is\ncurtain death",
|
||||
"Dark Aga Rooms\nare not a\nbright idea.",
|
||||
"Best advice\nfor a Goron?\nBe Boulder.",
|
||||
"Equestrian\nservices are\na stable job.",
|
||||
"Do I like\ndrills? Just\na bit.",
|
||||
]
|
||||
Ganon1_texts = [
|
||||
"Start your day\nsmiling with a\ndelicious\nwhole grain\nbreakfast\ncreated for\nyour\nincredible\ninsides.",
|
||||
|
@ -153,7 +186,7 @@ Ganon1_texts = [
|
|||
"Impa says that\nthe mark on\nyour hand\nmeans that you\nare the hero\nchosen to\nawaken Zelda.\nYour blood can\nresurrect me.",
|
||||
"Don't stand,\n\ndon't stand so\nDon't stand so\n\nclose to me\nDon't stand so\nclose to me\nBack off buddy",
|
||||
"So ya\nThought ya\nMight like to\ngo to the show\nTo feel the\nwarm thrill of\nconfusion\nThat space\ncadet glow.",
|
||||
"Like other\npulmonate land\ngastropods,\nthe majority\nof land slugs\nhave two pairs\nof 'feelers'\n,or tentacles,\non their head.",
|
||||
"Like other\npulmonate land\ngastropods,\nthe majority\nof land slugs\nhave two pairs\nof 'feelers'\nor tentacles,\non their head.",
|
||||
"If you were a\nburrito, what\nkind of a\nburrito would\nyou be?\nMe, I fancy I\nwould be a\nspicy barbacoa\nburrito.",
|
||||
"I am your\nfather's\nbrother's\nnephew's\ncousin's\nformer\nroommate. What\ndoes that make\nus, you ask?",
|
||||
"I'll be more\neager about\nencouraging\nthinking\noutside the\nbox when there\nis evidence of\nany thinking\ninside it.",
|
||||
|
@ -171,6 +204,8 @@ Ganon1_texts = [
|
|||
"When I conquer\nthe Light\nWorld, I'll\nhold a parade\nof all my\nmonsters to\ndemonstrate my\nmight to the\npeople!",
|
||||
"Life, dreams,\nhope...\nWhere'd they\ncome from? And\nwhere are they\nheaded? These\nthings... I am\ngoing to\ndestroy!",
|
||||
"My minions all\nfailed to\nguard those\nitems?!\n\nWhy am I\nsurrounded by\nincompetent\nfools?!",
|
||||
"Bacon dates to\n1500 BCE and\nrefers to the\nback of a pig.\nThe average\nAmerican eats\n18 pounds of\nRoman \"petaso\"\nevery year.",
|
||||
"The enrichment\nCenter would\nLike to remind\nYou that the\nCompanion\nDuck will not\nbetray you\nand in fact\ncannot speak.",
|
||||
"Goose is\nactually the\nterm for\nfemale geese,\nmale geese are\ncalled\nganders.",
|
||||
]
|
||||
TavernMan_texts = [
|
||||
|
@ -250,6 +285,20 @@ junk_texts = [
|
|||
"{C:GREEN}\nI bet a nice\ncup of tea\nwould help! >",
|
||||
"{C:GREEN}\nI bet you\nexpected help,\ndidn't you? >",
|
||||
"{C:GREEN}\nLearn to make\nplogues, easy\nand yummy! >",
|
||||
"{C:GREEN}\nI don't know\nwhere it is\neither. >",
|
||||
"{C:GREEN}\nA dog exists\nsomewhere. >",
|
||||
"{C:GREEN}\nIf all else\nfails use\nfire. >",
|
||||
"{C:GREEN}\nItems are\nrequired to\nwin. >",
|
||||
"{C:GREEN}\nDid you try\nchecking\nvanilla? >",
|
||||
"{C:GREEN}\n> If you find\nmy lunch,\ndon't eat it.",
|
||||
"{C:GREEN}\nDeadrocks are\nannoying. >",
|
||||
"{C:GREEN}\nMist Form\nis in the\nCatacombs. >",
|
||||
"{C:GREEN}\nMaybe you\ncould hire a\ndetective? >",
|
||||
"{C:GREEN}\n> READ\nor the owl\nwill eat you.",
|
||||
"{C:GREEN}\n> Bunnies\nare cute.",
|
||||
"{C:GREEN}\nPugs are the\nsuperior dog\nbreed. >",
|
||||
"{C:GREEN}\nYou are\nplaying\nALTTPR. >",
|
||||
"{C:GREEN}\nOther randos\nexist too!\nTry some! >",
|
||||
]
|
||||
|
||||
KingsReturn_texts = [
|
||||
|
|
35
Utils.py
35
Utils.py
|
@ -1,15 +1,21 @@
|
|||
from __future__ import annotations
|
||||
import typing
|
||||
|
||||
__version__ = "2.3.1"
|
||||
_version_tuple = tuple(int(piece, 10) for piece in __version__.split("."))
|
||||
|
||||
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
||||
return tuple(int(piece, 10) for piece in version.split("."))
|
||||
|
||||
|
||||
__version__ = "2.3.4"
|
||||
_version_tuple = tuplize_version(__version__)
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import functools
|
||||
|
||||
from yaml import load, dump
|
||||
from yaml import load, dump, safe_load
|
||||
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
|
@ -141,7 +147,7 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||
out_data[idx] = [int(new)]
|
||||
for offset in reversed(list(out_data.keys())):
|
||||
if offset - 1 in out_data:
|
||||
out_data[offset-1].extend(out_data.pop(offset))
|
||||
out_data[offset - 1].extend(out_data.pop(offset))
|
||||
with open('data/base2current.json', 'wt') as outfile:
|
||||
json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":"))
|
||||
|
||||
|
@ -150,7 +156,8 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||
return "New Rom Hash: " + basemd5.hexdigest()
|
||||
|
||||
|
||||
parse_yaml = functools.partial(load, Loader=Loader)
|
||||
parse_yaml = safe_load
|
||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
||||
|
||||
|
||||
class Hint(typing.NamedTuple):
|
||||
|
@ -190,6 +197,17 @@ def get_public_ipv4() -> str:
|
|||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
return ip
|
||||
|
||||
def get_public_ipv6() -> str:
|
||||
import socket
|
||||
import urllib.request
|
||||
import logging
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||
return ip
|
||||
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
|
@ -234,7 +252,7 @@ def persistent_load() -> typing.Dict[dict]:
|
|||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
storage = parse_yaml(f.read())
|
||||
storage = unsafe_parse_yaml(f.read())
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.debug(f"Could not read store: {e}")
|
||||
|
@ -244,7 +262,7 @@ def persistent_load() -> typing.Dict[dict]:
|
|||
return storage
|
||||
|
||||
|
||||
def get_adjuster_settings(romfile: str):
|
||||
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
|
||||
if hasattr(get_adjuster_settings, "adjuster_settings"):
|
||||
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
|
||||
else:
|
||||
|
@ -278,6 +296,7 @@ def get_adjuster_settings(romfile: str):
|
|||
get_adjuster_settings.adjuster_settings = adjuster_settings
|
||||
get_adjuster_settings.adjust_wanted = adjust_wanted
|
||||
return romfile, adjusted
|
||||
return romfile, False
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import os
|
||||
import multiprocessing
|
||||
import logging
|
||||
|
||||
from WebHost import app
|
||||
from waitress import serve
|
||||
|
||||
from WebHost.models import db, Room, db_session, select
|
||||
|
||||
DEBUG = False
|
||||
port = 80
|
||||
|
||||
|
||||
def autohost(config: dict):
|
||||
return
|
||||
# not implemented yet. https://github.com/ponyorm/pony/issues/527
|
||||
import time
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
def keep_running():
|
||||
# db.bind(**config["PONY"])
|
||||
# db.generate_mapping(check_tables=False)
|
||||
while 1:
|
||||
time.sleep(3)
|
||||
with db_session:
|
||||
rooms = select(
|
||||
room for room in Room if
|
||||
room.last_activity >= datetime.utcnow() - timedelta(hours=room.timeout))
|
||||
logging.info(rooms)
|
||||
|
||||
import threading
|
||||
threading.Thread(target=keep_running).start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method('spawn')
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
|
||||
configpath = "config.yaml"
|
||||
|
||||
if os.path.exists(configpath):
|
||||
import yaml
|
||||
with open(configpath) as c:
|
||||
app.config.update(yaml.safe_load(c))
|
||||
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
db.bind(**app.config["PONY"])
|
||||
db.generate_mapping(create_tables=True)
|
||||
if DEBUG:
|
||||
autohost(app.config)
|
||||
app.run(debug=True, port=port)
|
||||
else:
|
||||
serve(app, port=port, threads=app.config["WAITRESS_THREADS"])
|
|
@ -0,0 +1,140 @@
|
|||
"""Friendly reminder that if you want to host this somewhere on the internet, that it's licensed under MIT Berserker66
|
||||
So unless you're Berserker you need to include license information."""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import typing
|
||||
import multiprocessing
|
||||
import threading
|
||||
|
||||
from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort
|
||||
from flask_caching import Cache
|
||||
from flaskext.autoversion import Autoversion
|
||||
from flask_compress import Compress
|
||||
|
||||
from .models import *
|
||||
|
||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||
LOGS_FOLDER = os.path.relpath('logs')
|
||||
os.makedirs(LOGS_FOLDER, exist_ok=True)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('multidata', ".zip"))
|
||||
|
||||
app = Flask(__name__)
|
||||
Pony(app)
|
||||
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
||||
# if you want persistent sessions on your server, make sure you make this a constant in your config.yaml
|
||||
app.config["SECRET_KEY"] = os.urandom(32)
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
app.config[
|
||||
"WAITRESS_THREADS"] = 10 # waitress uses one thread for I/O, these are for processing of views that then get sent
|
||||
app.config["PONY"] = {
|
||||
'provider': 'sqlite',
|
||||
'filename': os.path.abspath('db.db3'),
|
||||
'create_db': True
|
||||
}
|
||||
app.config["CACHE_TYPE"] = "simple"
|
||||
app.autoversion = True
|
||||
av = Autoversion(app)
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
# this local cache is risky business if app hosting is done with subprocesses as it will not sync. Waitress is fine though
|
||||
multiworlds = {}
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
class MultiworldInstance():
|
||||
def __init__(self, room: Room):
|
||||
self.room_id = room.id
|
||||
self.process: typing.Optional[multiprocessing.Process] = None
|
||||
multiworlds[self.room_id] = self
|
||||
|
||||
def start(self):
|
||||
if self.process and self.process.is_alive():
|
||||
return False
|
||||
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
with db_session:
|
||||
self.process = multiprocessing.Process(group=None, target=run_server_process,
|
||||
args=(self.room_id, app.config["PONY"]),
|
||||
name="MultiHost")
|
||||
self.process.start()
|
||||
|
||||
def stop(self):
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
|
||||
|
||||
@app.route('/seed/<uuid:seed>')
|
||||
def view_seed(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
return render_template("view_seed.html", seed=seed,
|
||||
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
||||
|
||||
|
||||
@app.route('/new_room/<uuid:seed>')
|
||||
def new_room(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||
commit()
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
|
||||
|
||||
def _read_log(path: str):
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding="utf-8-sig") as log:
|
||||
yield from log
|
||||
else:
|
||||
yield f"Logfile {path} does not exist. " \
|
||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||
|
||||
|
||||
@app.route('/log/<uuid:room>')
|
||||
def display_log(room: UUID):
|
||||
# noinspection PyTypeChecker
|
||||
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||
|
||||
|
||||
processstartlock = threading.Lock()
|
||||
|
||||
|
||||
@app.route('/hosted/<uuid:room>', methods=['GET', 'POST'])
|
||||
def host_room(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
if request.method == "POST":
|
||||
if room.owner == session["_id"]:
|
||||
cmd = request.form["cmd"]
|
||||
Command(room=room, commandtext=cmd)
|
||||
commit()
|
||||
with db_session:
|
||||
multiworld = multiworlds.get(room.id, None)
|
||||
if not multiworld:
|
||||
multiworld = MultiworldInstance(room)
|
||||
|
||||
with processstartlock:
|
||||
multiworld.start()
|
||||
|
||||
return render_template("host_room.html", room=room)
|
||||
|
||||
|
||||
from WebHost.customserver import run_server_process
|
||||
from . import tracker, upload, landing # to trigger app routing picking up on it
|
|
@ -0,0 +1,134 @@
|
|||
import functools
|
||||
import logging
|
||||
import os
|
||||
import websockets
|
||||
import asyncio
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
|
||||
from WebHost import LOGS_FOLDER
|
||||
from .models import *
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||
from Utils import get_public_ipv4, get_public_ipv6
|
||||
|
||||
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
def _cmd_video(self, platform, user):
|
||||
"""Set a link for your name in the WebHost tracker pointing to a video stream"""
|
||||
if platform.lower().startswith("t"): # twitch
|
||||
self.ctx.video[self.client.team, self.client.slot] = "Twitch", user
|
||||
self.ctx.save()
|
||||
self.output(f"Registered Twitch Stream https://www.twitch.tv/{user}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# inject
|
||||
import MultiServer
|
||||
|
||||
MultiServer.client_message_processor = CustomClientMessageProcessor
|
||||
del (MultiServer)
|
||||
|
||||
|
||||
class DBCommandProcessor(ServerCommandProcessor):
|
||||
def output(self, text: str):
|
||||
logging.info(text)
|
||||
|
||||
|
||||
class WebHostContext(Context):
|
||||
def __init__(self):
|
||||
super(WebHostContext, self).__init__("", 0, "", 1, 40, True, "enabled", "enabled", 0)
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
self.tags = ["Berserker", "WebHost"]
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
while self.running:
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||
if commands:
|
||||
for command in commands:
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
time.sleep(5)
|
||||
|
||||
@db_session
|
||||
def load(self, room_id: int):
|
||||
self.room_id = room_id
|
||||
room = Room.get(id=room_id)
|
||||
if room.last_port:
|
||||
self.port = room.last_port
|
||||
else:
|
||||
self.port = get_random_port()
|
||||
return self._load(room.seed.multidata, True)
|
||||
|
||||
@db_session
|
||||
def init_save(self, enabled: bool = True):
|
||||
self.saving = enabled
|
||||
if self.saving:
|
||||
existings_savegame = Room.get(id=self.room_id).multisave
|
||||
if existings_savegame:
|
||||
self.set_save(existings_savegame)
|
||||
self._start_async_saving()
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
|
||||
@db_session
|
||||
def _save(self) -> bool:
|
||||
Room.get(id=self.room_id).multisave = self.get_save()
|
||||
return True
|
||||
|
||||
def get_save(self) -> dict:
|
||||
d = super(WebHostContext, self).get_save()
|
||||
d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()]
|
||||
return d
|
||||
|
||||
def get_random_port():
|
||||
return random.randint(49152, 65535)
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict):
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s',
|
||||
level=logging.INFO,
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.join(LOGS_FOLDER, f"{room_id}.txt"), 'a', 'utf-8-sig')])
|
||||
ctx = WebHostContext()
|
||||
ctx.load(room_id)
|
||||
ctx.auto_shutdown = 24 * 60 * 60 # 24 hours
|
||||
ctx.init_save()
|
||||
|
||||
try:
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
|
||||
await ctx.server
|
||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
|
||||
ping_interval=None)
|
||||
|
||||
await ctx.server
|
||||
for wssocket in ctx.server.ws_server.sockets:
|
||||
socketname = wssocket.getsockname()
|
||||
if wssocket.family == socket.AF_INET6:
|
||||
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
||||
with db_session:
|
||||
room = Room.get(id=ctx.room_id)
|
||||
room.last_port = socketname[1]
|
||||
elif wssocket.family == socket.AF_INET:
|
||||
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
||||
ctx.auto_shutdown = 6 * 60
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
logging.info("Shutting down")
|
||||
|
||||
asyncio.run(main())
|
|
@ -0,0 +1,8 @@
|
|||
from flask import render_template
|
||||
from WebHost import app, cache
|
||||
|
||||
|
||||
@cache.memoize(timeout=300)
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def landing():
|
||||
return render_template("landing.html")
|
|
@ -0,0 +1,42 @@
|
|||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
from pony.orm import *
|
||||
|
||||
db = Database()
|
||||
|
||||
|
||||
class Patch(db.Entity):
|
||||
id = PrimaryKey(int, auto=True)
|
||||
player = Required(int)
|
||||
data = Required(buffer, lazy=True)
|
||||
seed = Optional('Seed')
|
||||
|
||||
|
||||
class Room(db.Entity):
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||
owner = Required(UUID, index=True)
|
||||
commands = Set('Command')
|
||||
seed = Required('Seed', index=True)
|
||||
multisave = Optional(Json, lazy=True)
|
||||
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
|
||||
timeout = Required(int, default=lambda: 6)
|
||||
tracker = Optional(UUID, index=True)
|
||||
last_port = Optional(int, default=lambda: 0)
|
||||
|
||||
|
||||
class Seed(db.Entity):
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
rooms = Set(Room)
|
||||
multidata = Optional(Json, lazy=True)
|
||||
owner = Required(UUID, index=True)
|
||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||
patches = Set(Patch)
|
||||
spoiler = Optional(str, lazy=True)
|
||||
|
||||
|
||||
class Command(db.Entity):
|
||||
id = PrimaryKey(int, auto=True)
|
||||
room = Required(Room)
|
||||
commandtext = Required(str)
|
|
@ -0,0 +1,6 @@
|
|||
flask>=1.1.2
|
||||
pony>=0.7.13
|
||||
waitress>=1.4.4
|
||||
flask-caching>=1.9.0
|
||||
Flask-Autoversion>=0.2.0
|
||||
Flask-Compress>=1.5.0
|
|
@ -0,0 +1,114 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jquery')) :
|
||||
typeof define === 'function' && define.amd ? define(['exports', 'jquery'], factory) :
|
||||
(factory((global.$ = global.$ || {}, global.$.fn = global.$.fn || {}), global.$));
|
||||
}(this, (function (exports, $) {
|
||||
'use strict';
|
||||
|
||||
$ = $ && $.hasOwnProperty('default') ? $['default'] : $;
|
||||
|
||||
// 参考了(reference):
|
||||
// debouncing function from John Hann
|
||||
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
|
||||
function debounce(func, threshold) {
|
||||
var timeout;
|
||||
return function debounced() {
|
||||
var obj = this, args = arguments;
|
||||
|
||||
function delayed() {
|
||||
// 让调用smartresize的对象执行
|
||||
func.apply(obj, args);
|
||||
/*
|
||||
timeout = null;:这个语句只是单纯将timeout指向null,
|
||||
而timeout指向的定时器还存在,
|
||||
要想清除定时器(让setTimeout调用的函数不执行)要用clearTimeout(timeout)。
|
||||
eg:
|
||||
var timeout = setTimeout(function(){
|
||||
alert('timeout = null');// 执行
|
||||
},1000);
|
||||
timeout = null;
|
||||
var timeout = setTimeout(function(){
|
||||
alert('clearTimeout(timeout)');// 不执行
|
||||
},1000);
|
||||
clearTimeout(timeout);
|
||||
var timeout = setTimeout(function(){
|
||||
clearTimeout(timeout);
|
||||
alert('clearTimeout(timeout)');// 执行(已经开始执行匿名函数了)
|
||||
},1000);
|
||||
*/
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
// 如果有timeout正在倒计时,则清除当前timeout
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(delayed, threshold || 100);
|
||||
};
|
||||
}
|
||||
|
||||
function smartscroll(fn, threshold) {
|
||||
return fn ? this.bind('scroll', debounce(fn, threshold)) : this.trigger('smartscroll');
|
||||
}
|
||||
|
||||
//jquery-smartscroll
|
||||
$.fn.smartscroll = smartscroll;
|
||||
|
||||
function scrollsync(options) {
|
||||
var defaluts = {
|
||||
x_sync: true,
|
||||
y_sync: true,
|
||||
use_smartscroll: false,
|
||||
smartscroll_delay: 10,
|
||||
};
|
||||
|
||||
// 使用jQuery.extend 覆盖插件默认参数
|
||||
var options = $.extend({}, defaluts, options);
|
||||
console.log(options);
|
||||
|
||||
var scroll_type = options.use_smartscroll ? 'smartscroll' : 'scroll';
|
||||
var $containers = this;
|
||||
|
||||
// 滚动后设置scrolling的值,调用set同步滚动条
|
||||
var scrolling = {};
|
||||
Object.defineProperty(scrolling, 'top', {
|
||||
set: function (val) {
|
||||
$containers.each(function () {
|
||||
$(this).scrollTop(val);
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(scrolling, 'left', {
|
||||
set: function (val) {
|
||||
$containers.each(function () {
|
||||
$(this).scrollLeft(val);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$containers.on({
|
||||
mouseover: function () {
|
||||
if (scroll_type == 'smartscroll') {
|
||||
$(this).smartscroll(function () {
|
||||
options.x_sync && (scrolling.top = $(this).scrollTop());
|
||||
options.y_sync && (scrolling.left = $(this).scrollLeft());
|
||||
}, options.smartscroll_delay);
|
||||
return;
|
||||
}
|
||||
$(this).bind('scroll', function () {
|
||||
options.x_sync && (scrolling.top = $(this).scrollTop());
|
||||
options.y_sync && (scrolling.left = $(this).scrollLeft());
|
||||
});
|
||||
},
|
||||
mouseout: function () {
|
||||
$(this).unbind('scroll');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
exports.scrollsync = scrollsync;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {value: true});
|
||||
|
||||
})));
|
|
@ -0,0 +1,31 @@
|
|||
table.dataTable.table-sm > thead > tr > th :not(.sorting_disabled) {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.dataTable > thead > tr > th[class*="sort"]:before,
|
||||
.dataTable > thead > tr > th[class*="sort"]:after {
|
||||
content: "" !important;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 1px !important;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
img.alttp-sprite {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* this is specific to the tracker right now */
|
||||
@media all and (max-width: 1750px) {
|
||||
img.alttp-sprite {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id }}</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
{% if room.owner == session["_id"] %}
|
||||
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
|
||||
{% endif %}
|
||||
{% if room.tracker %}
|
||||
This room has a <a href="{{ url_for("get_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.<br>
|
||||
{% endif %}
|
||||
This room will be closed after {{ room.timeout }} hours of inactivity. Should you wish to continue later,
|
||||
you can simply refresh this page and the server will be started again.<br>
|
||||
{% if room.owner == session["_id"] %}
|
||||
<form method=post>
|
||||
<div class="form-group">
|
||||
<label for="cmd"></label>
|
||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||
placeholder="Server Command. /help to list them, list gets appended to log.">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
Log:
|
||||
<div id="logger"></div>
|
||||
<script>
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
var url = '{{ url_for('display_log', room = room.id) }}';
|
||||
|
||||
xmlhttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
document.getElementById("logger").innerText = this.responseText;
|
||||
}
|
||||
};
|
||||
|
||||
function request_new() {
|
||||
xmlhttp.open("GET", url, true);
|
||||
xmlhttp.send();
|
||||
}
|
||||
|
||||
window.setTimeout(request_new, 1000);
|
||||
window.setInterval(request_new, 3000);
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,47 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block head %}
|
||||
<title>Berserker's Multiworld</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<nav class="navbar navbar-dark bg-dark navbar-expand-sm">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for("uploads") }}">Start a Group</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for("uploads") }}">Upload a Multiworld</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for("uploads") }}">Your Content</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<br>
|
||||
<div class="container container-fluid">
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="container">
|
||||
<div class="col-md-5 p-lg-2 mx-auto my-2">
|
||||
<h1 class="text-center display-4 font-weight-normal">Berserker's Multiworld</h1>
|
||||
<p class="text-center lead font-weight-normal"><a
|
||||
href="https://github.com/Berserker66/MultiWorld-Utilities">Source Code</a>
|
||||
- <a href="https://github.com/Berserker66/MultiWorld-Utilities/wiki">Wiki</a>
|
||||
-
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/graphs/contributors">Contributors</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="lead">This is a randomizer for The Legend of Zelda: A Link to the Past.</p>
|
||||
<p class="lead">It is a multiworld, meaning items get shuffled across multiple players' worlds
|
||||
which get exchanged on pickup through the internet.</p>
|
||||
<p class="lead">This website allows hosting such a Multiworld and comes with an item and location
|
||||
tracker.</p>
|
||||
<p class="lead">Currently you still require a locally installed client to play, that handles
|
||||
connecting to the server and patching a vanilla game to the randomized one. Get started on the
|
||||
<a href="https://github.com/Berserker66/MultiWorld-Utilities/wiki">Wiki</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
||||
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||
{% block head %}<title>Berserker's Multiworld</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class=".container-fluid">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-danger" role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<br> {# spacing for notice #}
|
||||
<footer class="page-footer" style="position: fixed; left: 0; bottom: 0; width: 100%; text-align: center">
|
||||
<div class="container">
|
||||
<span class="text-muted">This site uses a cookie to track your session in order to give you ownership over uploaded files and created instances.</span>
|
||||
{# <button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('cookiefooter').remove()">X</button> #}
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
|
@ -0,0 +1,10 @@
|
|||
{% macro list_rooms(rooms) -%}
|
||||
Rooms:
|
||||
<ul class="list-group">
|
||||
{% for room in rooms %}
|
||||
<li class="list-group-item"><a href="{{ url_for("host_room", room=room.id) }}">Room #{{ room.id }}</a></li>
|
||||
{% endfor %}
|
||||
{{ caller() }}
|
||||
|
||||
</ul>
|
||||
{%- endmacro %}
|
|
@ -0,0 +1,172 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block head %}
|
||||
<title>Multiworld Tracker for Room {{ room.id }}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="https://cdn.datatables.net/v/bs4/jq-3.3.1/dt-1.10.21/fh-3.1.7/datatables.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static_autoversion("static.css") }}"/>
|
||||
<script type="text/javascript"
|
||||
src="https://cdn.datatables.net/v/bs4/jq-3.3.1/dt-1.10.21/fh-3.1.7/datatables.min.js"></script>
|
||||
<script src="{{ static_autoversion("jquery.scrollsync.js") }}"></script>
|
||||
|
||||
<script>
|
||||
|
||||
$(document).ready(function () {
|
||||
var tables = $(".table").DataTable({
|
||||
"paging": false,
|
||||
"ordering": true,
|
||||
"info": false,
|
||||
"dom": "t",
|
||||
"scrollY": "39vh",
|
||||
"scrollCollapse": true,
|
||||
});
|
||||
|
||||
$('#searchbox').keyup(function () {
|
||||
tables.search($(this).val()).draw();
|
||||
});
|
||||
|
||||
function update() {
|
||||
var target = $("<div></div>");
|
||||
target.load("/tracker/{{ room.tracker }}", function (response, status) {
|
||||
if (status === "success") {
|
||||
target.find(".table").each(function (i, new_table) {
|
||||
var new_trs = $(new_table).find("tbody>tr");
|
||||
var old_table = tables.eq(i);
|
||||
var topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||
var leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||
old_table.clear();
|
||||
old_table.rows.add(new_trs).draw();
|
||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||
});
|
||||
} else {
|
||||
console.log("Failed to connect to Server, in order to update Table Data.");
|
||||
console.log(response);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setInterval(update, 30000);
|
||||
|
||||
$(".dataTables_scrollBody").scrollsync({
|
||||
y_sync: true,
|
||||
x_sync: true
|
||||
})
|
||||
$(window).resize(function () {
|
||||
tables.draw();
|
||||
});
|
||||
setTimeout(
|
||||
tables.draw, {# this fixes the top header misalignment, for some reason #}
|
||||
500
|
||||
);
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<input id="searchbox" class="form-control" type="text" placeholder="Search">
|
||||
<div>
|
||||
{% for team, players in inventory.items() %}
|
||||
<table class="table table-striped table-bordered table-hover table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
{% for name in tracking_names %}
|
||||
{% if name in icons %}
|
||||
<th style="text-align: center"><img class="alttp-sprite"
|
||||
src="{{ icons[name] }}"
|
||||
alt="{{ name|e }}"></th>
|
||||
{% else %}
|
||||
<th>{{ name|e }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player, items in players.items() %}
|
||||
<tr>
|
||||
<td class="table-info">{{ loop.index }}</td>
|
||||
{% if (team, loop.index) in video %}
|
||||
<td class="table-info">
|
||||
<a target="_blank" href="https://www.twitch.tv/{{ video[(team, loop.index)][1] }}">
|
||||
{{ player_names[(team, loop.index)] }}
|
||||
▶️</a></td>
|
||||
{% else %}
|
||||
<td class="table-info">{{ player_names[(team, loop.index)] }}</td>{% endif %}
|
||||
{% for id in tracking_ids %}
|
||||
|
||||
{% if items[id] %}
|
||||
<td style="text-align: center" class="table-success">
|
||||
{% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
{% for team, players in checks_done.items() %}
|
||||
<table class="table table-striped table-bordered table-hover table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = (3 if area in key_locations else 1) %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" style="text-align: center"><img class="alttp-sprite"
|
||||
src="{{ icons[area] }}"
|
||||
alt="{{ area }}"></th>
|
||||
{% else %}
|
||||
<th colspan="{{ colspan }}">{{ area }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th rowspan="2">Last Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
<th style="text-align: center"><img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th style="text-align: center"><img class="alttp-sprite"
|
||||
src="{{ icons["Small Key"] }}" alt="Small Key"></th>
|
||||
<th style="text-align: center"><img class="alttp-sprite"
|
||||
src="{{ icons["Big Key"] }}" alt="Big Key"></th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player, checks in players.items() %}
|
||||
<tr>
|
||||
<td class="table-info">{{ loop.index }}</td>
|
||||
<td class="table-info">{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{% for area in ordered_areas %}
|
||||
{% set checks_done = checks[area] %}
|
||||
{% set checks_total = checks_in_area[area] %}
|
||||
{% if checks_done == checks_total %}
|
||||
<td style="text-align: center" class="table-success">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{% else %}
|
||||
<td style="text-align: center">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{% endif %}
|
||||
{% if area in key_locations %}
|
||||
<td>{{ inventory[team][player][small_key_ids[area]] }}</td>
|
||||
<td>{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if activity_timers[(team, player)] %}
|
||||
<td class="table-info">{{ activity_timers[(team, player)] | render_timedelta }}</td>
|
||||
{% else %}
|
||||
<td class="table-warning">None</td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% block head %}
|
||||
<title>Upload Multidata</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<h1>Upload Multidata or Multiworld Zip</h1>
|
||||
<form method=post enctype=multipart/form-data>
|
||||
<input type=file name=file>
|
||||
<input type=submit value=Upload>
|
||||
</form>
|
||||
<br>
|
||||
{% if rooms %}
|
||||
<h1>Your Rooms:</h1>
|
||||
<ul class="list-group">
|
||||
{% for room in rooms %}
|
||||
<li class="list-group-item"><a href="{{ url_for("host_room", room=room.id) }}">Room #{{ room.id }}</a>
|
||||
based on <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h3>No rooms owned by you were found. Upload a Multiworld to get started.</h3>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,25 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% block head %}
|
||||
<title>Multiworld Seed {{ seed.id }}</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
Seed #{{ seed.id }}<br>
|
||||
Created: {{ seed.creation_time }} UTC <br>
|
||||
Players:
|
||||
<ul class="list-group">
|
||||
{% for team in seed.multidata["names"] %}
|
||||
<li class="list-group-item">Team #{{ loop.index }} - {{ team | length }}
|
||||
<ul class="list-group">
|
||||
{% for player in team %}
|
||||
<li class="list-group-item">{{ player }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% call macros.list_rooms(rooms) %}
|
||||
<li class="list-group-item list-group-item-action"><a href="{{ url_for("new_room", seed=seed.id) }}">new
|
||||
room</a></li>
|
||||
{% endcall %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,266 @@
|
|||
import collections
|
||||
|
||||
from flask import render_template
|
||||
from werkzeug.exceptions import abort
|
||||
import datetime
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
import Items
|
||||
from WebHost import app, cache, Room
|
||||
|
||||
|
||||
def get_id(item_name):
|
||||
return Items.item_table[item_name][3]
|
||||
|
||||
|
||||
icons = {
|
||||
"Progressive Sword":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/53/ALttP_Titan's_Mitt_Sprite.png?version=6ac54c3016a23b94413784881fcd3c75",
|
||||
"Flippers":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/88/ALttP_Zora's_Flippers_Sprite.png?version=b9d7521bb3a5a4d986879f70a70bc3da",
|
||||
"Moon Pearl":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=deaf51f8636823558bd6e6307435fb01",
|
||||
"Fire Rod":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Small Key":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest":
|
||||
r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda"
|
||||
}
|
||||
|
||||
links = {"Bow": "Progressive Bow",
|
||||
"Silver Arrows": "Progressive Bow",
|
||||
"Silver Bow": "Progressive Bow",
|
||||
"Progressive Bow (Alt)": "Progressive Bow",
|
||||
"Bottle (Red Potion)": "Bottle",
|
||||
"Bottle (Green Potion)": "Bottle",
|
||||
"Bottle (Blue Potion)": "Bottle",
|
||||
"Bottle (Fairy)": "Bottle",
|
||||
"Bottle (Bee)": "Bottle",
|
||||
"Bottle (Good Bee)": "Bottle",
|
||||
"Fighter Sword": "Progressive Sword",
|
||||
"Master Sword": "Progressive Sword",
|
||||
"Tempered Sword": "Progressive Sword",
|
||||
"Golden Sword": "Progressive Sword",
|
||||
"Power Glove": "Progressive Glove",
|
||||
"Titans Mitts": "Progressive Glove"
|
||||
}
|
||||
|
||||
levels = {"Fighter Sword": 1,
|
||||
"Master Sword": 2,
|
||||
"Tempered Sword": 3,
|
||||
"Golden Sword": 4,
|
||||
"Power Glove": 1,
|
||||
"Titans Mitts": 2,
|
||||
"Silver Bow": 2}
|
||||
|
||||
multi_items = {get_id(name) for name in ("Progressive Sword", "Progressive Bow", "Bottle", "Progressive Glove")}
|
||||
links = {get_id(key): get_id(value) for key, value in links.items()}
|
||||
levels = {get_id(key): value for key, value in levels.items()}
|
||||
|
||||
tracking_names = ["Progressive Sword", "Progressive Bow", "Book of Mudora", "Hammer",
|
||||
"Hookshot", "Magic Mirror", "Flute",
|
||||
"Pegasus Boots", "Progressive Glove", "Flippers", "Moon Pearl", "Blue Boomerang",
|
||||
"Red Boomerang", "Bug Catching Net", "Cape", "Shovel", "Lamp",
|
||||
"Mushroom", "Magic Powder",
|
||||
"Cane of Somaria", "Cane of Byrna", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake",
|
||||
"Bottle", "Triforce"] # TODO make sure this list has what we need and sort it better
|
||||
|
||||
default_locations = {
|
||||
'Light World': {1572864, 1572865, 60034, 1572867, 1572868, 60037, 1572869, 1572866, 60040, 59788, 60046, 60175,
|
||||
1572880, 60049, 60178, 1572883, 60052, 60181, 1572885, 60055, 60184, 191256, 60058, 60187, 1572884,
|
||||
1572886, 1572887, 1572906, 60202, 60205, 59824, 166320, 1010170, 60208, 60211, 60214, 60217, 59836,
|
||||
60220, 60223, 59839, 1573184, 60226, 975299, 1573188, 1573189, 188229, 60229, 60232, 1573193,
|
||||
1573194, 60235, 1573187, 59845, 59854, 211407, 60238, 59857, 1573185, 1573186, 1572882, 212328,
|
||||
59881, 59761, 59890, 59770, 193020, 212605},
|
||||
'Dark World': {59776, 59779, 975237, 1572870, 60043, 1572881, 60190, 60193, 60196, 60199, 60840, 1573190, 209095,
|
||||
1573192, 1573191, 60241, 60244, 60247, 60250, 59884, 59887, 60019, 60022, 60028, 60031},
|
||||
'Desert Palace': {1573216, 59842, 59851, 59791, 1573201, 59830},
|
||||
'Eastern Palace': {1573200, 59827, 59893, 59767, 59833, 59773},
|
||||
'Hyrule Castle': {60256, 60259, 60169, 60172, 59758, 59764, 60025, 60253},
|
||||
'Agahnims Tower': {60082, 60085},
|
||||
'Tower of Hera': {1573218, 59878, 59821, 1573202, 59896, 59899},
|
||||
'Swamp Palace': {60064, 60067, 60070, 59782, 59785, 60073, 60076, 60079, 1573204, 60061},
|
||||
'Thieves Town': {59905, 59908, 59911, 59914, 59917, 59920, 59923, 1573206},
|
||||
'Skull Woods': {59809, 59902, 59848, 59794, 1573205, 59800, 59803, 59806},
|
||||
'Ice Palace': {59872, 59875, 59812, 59818, 59860, 59797, 1573207, 59869},
|
||||
'Misery Mire': {60001, 60004, 60007, 60010, 60013, 1573208, 59866, 59998},
|
||||
'Turtle Rock': {59938, 59941, 59944, 1573209, 59947, 59950, 59953, 59956, 59926, 59929, 59932, 59935},
|
||||
'Palace of Darkness': {59968, 59971, 59974, 59977, 59980, 59983, 59986, 1573203, 59989, 59959, 59992, 59962, 59995,
|
||||
59965},
|
||||
'Ganons Tower': {60160, 60163, 60166, 60088, 60091, 60094, 60097, 60100, 60103, 60106, 60109, 60112, 60115, 60118,
|
||||
60121, 60124, 60127, 1573217, 60130, 60133, 60136, 60139, 60142, 60145, 60148, 60151, 60157},
|
||||
'Total': set()}
|
||||
|
||||
key_locations = {"Desert Palace", "Eastern Palace", "Hyrule Castle", "Agahnims Tower", "Tower of Hera", "Swamp Palace",
|
||||
"Thieves Town", "Skull Woods", "Ice Palace", "Misery Mire", "Turtle Rock", "Palace of Darkness",
|
||||
"Ganons Tower"}
|
||||
|
||||
location_to_area = {}
|
||||
for area, locations in default_locations.items():
|
||||
for location in locations:
|
||||
location_to_area[location] = area
|
||||
|
||||
checks_in_area = {area: len(checks) for area, checks in default_locations.items()}
|
||||
checks_in_area["Total"] = 216
|
||||
|
||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
|
||||
tracking_ids = []
|
||||
|
||||
for item in tracking_names:
|
||||
tracking_ids.append(get_id(item))
|
||||
|
||||
small_key_ids = {}
|
||||
big_key_ids = {}
|
||||
|
||||
for item_name, data in Items.item_table.items():
|
||||
if "Key" in item_name:
|
||||
area = item_name.split("(")[1][:-1]
|
||||
if "Small" in item_name:
|
||||
small_key_ids[area] = data[3]
|
||||
else:
|
||||
big_key_ids[area] = data[3]
|
||||
|
||||
from MultiServer import get_item_name_from_id
|
||||
|
||||
|
||||
def attribute_item(inventory, team, recipient, item):
|
||||
target_item = links.get(item, item)
|
||||
if item in levels: # non-progressive
|
||||
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
|
||||
else:
|
||||
inventory[team][recipient][target_item] += 1
|
||||
|
||||
|
||||
@app.template_filter()
|
||||
def render_timedelta(delta: datetime.timedelta):
|
||||
hours, minutes = divmod(delta.total_seconds() / 60, 60)
|
||||
hours = str(int(hours))
|
||||
minutes = str(int(minutes)).zfill(2)
|
||||
return f"{hours}:{minutes}"
|
||||
|
||||
|
||||
_multidata_cache = {}
|
||||
|
||||
|
||||
def get_static_room_data(room: Room):
|
||||
result = _multidata_cache.get(room.seed.id, None)
|
||||
if result:
|
||||
return result
|
||||
multidata = room.seed.multidata
|
||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
||||
names = multidata["names"]
|
||||
_multidata_cache[room.seed.id] = locations, names
|
||||
return locations, names
|
||||
|
||||
|
||||
@app.route('/tracker/<uuid:tracker>')
|
||||
@cache.memoize(timeout=30) # update every 30 seconds
|
||||
def get_tracker(tracker: UUID):
|
||||
room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names = get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1)}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
precollected_items = room.seed.multidata.get("precollected_items", None)
|
||||
|
||||
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
|
||||
if precollected_items:
|
||||
precollected = precollected_items[player - 1]
|
||||
for item_id in precollected:
|
||||
attribute_item(inventory, team, player, item_id)
|
||||
for location in locations_checked:
|
||||
item, recipient = locations[location, player]
|
||||
attribute_item(inventory, team, recipient, item)
|
||||
checks_done[team][player][location_to_area[location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
|
||||
for (team, player), game_state in room.multisave.get("client_game_state", []):
|
||||
if game_state:
|
||||
inventory[team][player][106] = 1 # Triforce
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
for team, names in enumerate(names):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
|
||||
for (team, player), alias in room.multisave.get("name_aliases", []):
|
||||
player_names[(team, player)] = alias
|
||||
|
||||
video = {}
|
||||
for (team, player), data in room.multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
|
||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||
checks_in_area=checks_in_area, activity_timers=activity_timers,
|
||||
key_locations=key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
||||
video=video)
|
|
@ -0,0 +1,73 @@
|
|||
import json
|
||||
import zlib
|
||||
import zipfile
|
||||
import logging
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from pony.orm import commit, select
|
||||
|
||||
from WebHost import app, allowed_file, Seed, Room, Patch
|
||||
|
||||
accepted_zip_contents = {"patches": ".bmbp",
|
||||
"spoiler": ".txt",
|
||||
"multidata": "multidata"}
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
def uploads():
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
else:
|
||||
file = request.files['file']
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
patches = set()
|
||||
spoiler = ""
|
||||
multidata = None
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||
elif file.filename.endswith(".bmbp"):
|
||||
player = int(file.filename.split("P")[-1].split(".")[0].split("_")[0])
|
||||
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
|
||||
elif file.filename.endswith(".txt"):
|
||||
spoiler = zfile.open(file, "rt").read().decode("utf-8-sig")
|
||||
elif file.filename.endswith("multidata"):
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
if multidata:
|
||||
commit() # commit patches
|
||||
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
||||
commit() # create seed
|
||||
for patch in patches:
|
||||
patch.seed = seed
|
||||
|
||||
return redirect(url_for("view_seed", seed=seed.id))
|
||||
else:
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
else:
|
||||
try:
|
||||
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
||||
except:
|
||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||
else:
|
||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||
commit() # place into DB and generate ids
|
||||
return redirect(url_for("view_seed", seed=seed.id))
|
||||
else:
|
||||
flash("Not recognized file format. Awaiting a .multidata file.")
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
return render_template("uploads.html", rooms=rooms)
|
41
WebUI.py
41
WebUI.py
|
@ -57,6 +57,7 @@ class WebUiClient(Node):
|
|||
'serverAddress': server_address,
|
||||
'server': 1 if ctx.server is not None and not ctx.server.socket.closed else 0,
|
||||
}))
|
||||
|
||||
def send_device_list(self, devices):
|
||||
self.broadcast_all(self.build_message('availableDevices', {
|
||||
'devices': devices,
|
||||
|
@ -105,11 +106,20 @@ class WebUiClient(Node):
|
|||
'entranceLocation': entrance_location,
|
||||
}))
|
||||
|
||||
def send_game_state(self, ctx: Context):
|
||||
self.broadcast_all(self.build_message('gameState', {
|
||||
'hintCost': 0,
|
||||
'checkPoints': 0,
|
||||
'playerPoints': 0,
|
||||
def send_game_info(self, ctx: Context):
|
||||
self.broadcast_all(self.build_message('gameInfo', {
|
||||
'serverVersion': Utils.__version__,
|
||||
'hintCost': ctx.hint_cost,
|
||||
'checkPoints': ctx.check_points,
|
||||
'forfeitMode': ctx.forfeit_mode,
|
||||
'remainingMode': ctx.remaining_mode,
|
||||
}))
|
||||
|
||||
def send_location_check(self, ctx: Context, last_check: str):
|
||||
self.broadcast_all(self.build_message('locationCheck', {
|
||||
'totalChecks': len(ctx.locations_checked),
|
||||
'hintPoints': ctx.hint_points,
|
||||
'lastCheck': last_check,
|
||||
}))
|
||||
|
||||
|
||||
|
@ -117,14 +127,27 @@ class WaitingForUiException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
webthread = None
|
||||
web_thread = None
|
||||
PORT = 5050
|
||||
Handler = partial(http.server.SimpleHTTPRequestHandler,
|
||||
|
||||
|
||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_request(self, code='-', size='-'):
|
||||
pass
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def log_date_time_string(self):
|
||||
pass
|
||||
|
||||
|
||||
Handler = partial(RequestHandler,
|
||||
directory=Utils.local_path(os.path.join("data", "web", "public")))
|
||||
|
||||
|
||||
def start_server(socket_port: int, on_start=lambda: None):
|
||||
global webthread
|
||||
global web_thread
|
||||
try:
|
||||
server = socketserver.TCPServer(("", PORT), Handler)
|
||||
except OSError:
|
||||
|
@ -144,4 +167,4 @@ def start_server(socket_port: int, on_start=lambda: None):
|
|||
else:
|
||||
print("serving at port", PORT)
|
||||
on_start()
|
||||
webthread = threading.Thread(target=server.serve_forever).start()
|
||||
web_thread = threading.Thread(target=server.serve_forever).start()
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
theme: jekyll-theme-slate
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue