diff --git a/BaseClasses.py b/BaseClasses.py index 1b3fa1bd..49997dc6 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -8,7 +8,6 @@ from EntranceShuffle import door_addresses from Utils import int16_as_bytes class World(object): - def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints): self.players = players self.teams = 1 @@ -953,7 +952,7 @@ class Shop(object): class Spoiler(object): - + world: World def __init__(self, world): self.world = world self.hashes = {} diff --git a/Items.py b/Items.py index f2d4e910..8feff346 100644 --- a/Items.py +++ b/Items.py @@ -51,39 +51,100 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla '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'), - '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'), - 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x01], None, None, None, None, None, None, None), - 'Red Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x02], None, None, None, None, None, None, None), - 'Blue Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x03], None, None, None, None, None, None, None), - 'Triforce': (True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), - 'Power Star': (True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), - 'Triforce Piece': (True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), - 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x06], None, None, None, None, None, None, None), - 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x06], None, None, None, None, None, None, None), - 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x06], None, None, None, None, None, None, None), - 'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x06], None, None, None, None, None, None, None), - 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x06], None, None, None, None, None, None, None), - 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], None, None, None, None, None, None, None), - 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x06], None, None, None, None, None, None, None), - 'Single Arrow': (False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), - 'Arrows (10)': (False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), - 'Arrow Upgrade (+10)': (False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Arrow Upgrade (+5)': (False, False, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), - 'Single Bomb': (False, False, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'), - 'Bombs (3)': (False, False, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'), - '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'), + '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'), + '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'), + 'Green Pendant': ( + True, False, 'Crystal', (0x04, 0x38, 0x62, 0x00, 0x69, 0x01), None, None, None, None, None, None, None), + 'Red Pendant': ( + True, False, 'Crystal', (0x02, 0x34, 0x60, 0x00, 0x69, 0x02), None, None, None, None, None, None, None), + 'Blue Pendant': ( + True, False, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, None), + 'Triforce': ( + True, False, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', + 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), + 'Power Star': ( + True, False, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', + 'see stars with shroom', 'mario powers up again', 'a Power Star'), + 'Triforce Piece': ( + True, False, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', + 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'), + 'Crystal 1': ( + True, False, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, None), + 'Crystal 2': ( + True, False, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, None), + 'Crystal 3': ( + True, False, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, None), + 'Crystal 4': ( + True, False, 'Crystal', (0x20, 0x34, 0x64, 0x40, 0x6D, 0x06), None, None, None, None, None, None, None), + 'Crystal 5': ( + True, False, 'Crystal', (0x04, 0x32, 0x64, 0x40, 0x6E, 0x06), None, None, None, None, None, None, None), + 'Crystal 6': ( + True, False, 'Crystal', (0x01, 0x32, 0x64, 0x40, 0x6F, 0x06), None, None, None, None, None, None, None), + 'Crystal 7': ( + True, False, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, None), + 'Single Arrow': ( + False, False, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', + 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), + 'Arrows (10)': ( + False, False, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', + 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', + 'ten arrows'), + 'Arrow Upgrade (+10)': ( + False, False, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', + 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', + 'arrow capacity'), + 'Arrow Upgrade (+5)': ( + False, False, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', + 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', + 'arrow capacity'), + 'Single Bomb': (False, False, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', + 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', + '\'splosion boy explodes again', 'a bomb'), + 'Bombs (3)': (False, False, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', + 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', + '\'splosion boy explodes again', 'three bombs'), + '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'), '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'), @@ -166,12 +227,20 @@ item_table = {'Bow': (True, False, None, 0x0B, 'You have\nchosen the\narcher cla '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'), - 'Bee': (False, False, None, 0x0E, '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 bee'), - 'Small Heart': (False, False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'), + 'Bee': (False, False, None, 0x0E, '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 bee'), + 'Small Heart': ( + False, False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', + 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'), 'Beat Agahnim 1': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Beat Agahnim 2': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Get Frog': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Return Smith': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Pick Up Purple Chest': (True, False, 'Event', None, None, None, None, None, None, None, None), 'Open Floodgate': (True, False, 'Event', None, None, None, None, None, None, None, None), - } + } + +lookup_lower_name_to_id = {name.lower(): data[3] for name, data in item_table.items()} +lookup_lower_name_to_name = {name.lower(): name for name in item_table} +lookup_id_to_name = {data[3]: name for name, data in item_table.items()} diff --git a/MultiClient.py b/MultiClient.py index e0909224..1ca7098c 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -17,6 +17,7 @@ import aioconsole import Items import Regions +import Utils class ReceivedItem: def __init__(self, item, location, player): @@ -59,15 +60,20 @@ class Context: self.rom = None self.auth = None + +color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, + 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, + 'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} + + def color_code(*args): - codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, - 'magenta': 35, 'cyan': 36, 'white': 37 , 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, - 'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} - return '\033[' + ';'.join([str(codes[arg]) for arg in args]) + 'm' + return '\033[' + ';'.join([str(color_codes[arg]) for arg in args]) + 'm' + def color(text, *args): return color_code(*args) + text + color_code('reset') + RECONNECT_DELAY = 30 ROM_START = 0x000000 @@ -604,15 +610,17 @@ async def process_server_cmd(ctx : Context, cmd, args): logging.info('--------------------------------') logging.info('Room Information:') logging.info('--------------------------------') + logging.info(f'Server protocol version: {args.get("version", "unknown Bonta Protocol")}') + if "tags" in args: + logging.info("Server protocol tags: " + ", ".join(args["tags"])) if args['password']: logging.info('Password required') if len(args['players']) < 1: logging.info('No player connected') else: args['players'].sort() - current_team = 0 + current_team = -1 logging.info('Connected players:') - logging.info(' Team #1') for team, slot, name in args['players']: if team != current_team: logging.info(f' Team #{team + 1}') @@ -620,18 +628,21 @@ async def process_server_cmd(ctx : Context, cmd, args): logging.info(' %s (Player %d)' % (name, slot)) await server_auth(ctx, args['password']) - if cmd == 'ConnectionRefused': + elif cmd == 'ConnectionRefused': if 'InvalidPassword' in args: logging.error('Invalid password') ctx.password = None await server_auth(ctx, True) if 'InvalidRom' in args: - raise Exception('Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes') + raise Exception( + 'Invalid ROM detected, please verify that you have loaded the correct rom and reconnect your snes (/snes)') if 'SlotAlreadyTaken' in args: raise Exception('Player slot already in use for that team') + if 'IncompatibleVersion' in args: + raise Exception('Server reported your client version as incompatible') raise Exception('Connection refused by the multiworld host') - if cmd == 'Connected': + elif cmd == 'Connected': ctx.team, ctx.slot = args[0] ctx.player_names = {p: n for p, n in args[1]} msgs = [] @@ -642,7 +653,7 @@ async def process_server_cmd(ctx : Context, cmd, args): if msgs: await send_msgs(ctx.socket, msgs) - if cmd == 'ReceivedItems': + elif cmd == 'ReceivedItems': start_index, items = args if start_index == 0: ctx.items_received = [] @@ -656,36 +667,52 @@ async def process_server_cmd(ctx : Context, cmd, args): ctx.items_received.append(ReceivedItem(*item)) ctx.watcher_event.set() - if cmd == 'LocationInfo': + elif cmd == 'LocationInfo': for location, item, player in args: if location not in ctx.locations_info: replacements = {0xA2: 'Small Key', 0x9D: 'Big Key', 0x8D: 'Compass', 0x7D: 'Map'} item_name = replacements.get(item, get_item_name_from_id(item)) - logging.info(f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}") + logging.info( + f"Saw {color(item_name, 'red', 'bold')} at {list(Regions.location_table.keys())[location - 1]}") ctx.locations_info[location] = (item, player) ctx.watcher_event.set() - if cmd == 'ItemSent': + elif cmd == 'ItemSent': player_sent, location, player_recvd, item = args item = color(get_item_name_from_id(item), 'cyan' if player_sent != ctx.slot else 'green') player_sent = color(ctx.player_names[player_sent], 'yellow' if player_sent != ctx.slot else 'magenta') player_recvd = color(ctx.player_names[player_recvd], 'yellow' if player_recvd != ctx.slot else 'magenta') - logging.info('%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location))) + logging.info( + '%s sent %s to %s (%s)' % (player_sent, item, player_recvd, get_location_name_from_address(location))) - if cmd == 'Print': + elif cmd == 'Hint': + hints = [Utils.Hint(*hint) for hint in args] + for hint in hints: + item = color(get_item_name_from_id(hint.item), 'green' if hint.found else 'cyan') + player_find = color(ctx.player_names[hint.finding_player], + 'yellow' if hint.finding_player != ctx.slot else 'magenta') + player_recvd = color(ctx.player_names[hint.receiving_player], + 'yellow' if hint.receiving_player != ctx.slot else 'magenta') + logging.info(f"[Hint]: {player_recvd}'s {item} can be found " + f"at {get_location_name_from_address(hint.location)} in {player_find}'s World." + + (" (found)" if hint.found else "")) + elif cmd == 'Print': logging.info(args) -async def server_auth(ctx : Context, password_requested): + +async def server_auth(ctx: Context, password_requested): if password_requested and not ctx.password: logging.info('Enter the password required to join this game:') ctx.password = await console_input(ctx) if ctx.rom is None: ctx.awaiting_rom = True - logging.info('No ROM detected, awaiting snes connection to authenticate to the multiworld server') + logging.info('No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)') return ctx.awaiting_rom = False ctx.auth = ctx.rom.copy() - await send_msgs(ctx.socket, [['Connect', {'password': ctx.password, 'rom': ctx.auth}]]) + await send_msgs(ctx.socket, [['Connect', { + 'password': ctx.password, 'rom': ctx.auth, 'version': [1, 0, 0], 'tags': ['Berserker'] + }]]) async def console_input(ctx : Context): ctx.input_requests += 1 @@ -714,38 +741,43 @@ async def console_loop(ctx : Context): if not command: continue - if command[0] == '/exit': - ctx.exit_event.set() + if command[0][:1] != '/': + asyncio.create_task(send_msgs(ctx.socket, [['Say', input]])) + continue - if command[0] == '/snes': + precommand = command[0][1:] + + if precommand == 'exit': + ctx.exit_event.set() + elif precommand == 'snes': ctx.snes_reconnect_address = None asyncio.create_task(snes_connect(ctx, command[1] if len(command) > 1 else ctx.snes_address)) - if command[0] in ['/snes_close', '/snes_quit']: + elif precommand in {'snes_close', 'snes_quit'}: ctx.snes_reconnect_address = None if ctx.snes_socket is not None and not ctx.snes_socket.closed: await ctx.snes_socket.close() - if command[0] in ['/connect', '/reconnect']: + elif precommand in {'connect', 'reconnect'}: ctx.server_address = None asyncio.create_task(connect(ctx, command[1] if len(command) > 1 else None)) - if command[0] == '/disconnect': + elif precommand == 'disconnect': ctx.server_address = None asyncio.create_task(disconnect(ctx)) - if command[0][:1] != '/': - asyncio.create_task(send_msgs(ctx.socket, [['Say', input]])) - if command[0] == '/received': + + elif precommand == 'received': logging.info('Received items:') for index, item in enumerate(ctx.items_received, 1): logging.info('%s from %s (%s) (%d/%d in list)' % ( - color(get_item_name_from_id(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), + color(get_item_name_from_id(item.item), 'red', 'bold'), + color(ctx.player_names[item.player], 'yellow'), get_location_name_from_address(item.location), index, len(ctx.items_received))) - if command[0] == '/missing': + elif precommand == 'missing': for location in [k for k, v in Regions.location_table.items() if type(v[0]) is int]: if location not in ctx.locations_checked: logging.info('Missing: ' + location) - if command[0] == '/getitem' and len(command) > 1: + elif precommand == 'getitem' and len(command) > 1: item = input[9:] item_id = Items.item_table[item][3] if item in Items.item_table else None if type(item_id) is int and item_id in range(0x100): @@ -754,9 +786,10 @@ async def console_loop(ctx : Context): snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([0])) else: logging.info('Invalid item: ' + item) - if command[0] == "/license": + elif precommand == "license": with open("LICENSE") as f: logging.info(f.read()) + await snes_flush_writes(ctx) def get_item_name_from_id(code): diff --git a/MultiServer.py b/MultiServer.py index 39ccaf4b..4a3d0dff 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -8,6 +8,7 @@ import shlex import urllib.request import zlib import collections +import typing import ModuleUpdate ModuleUpdate.update() @@ -20,7 +21,11 @@ import Regions import Utils from MultiClient import ReceivedItem, get_item_name_from_id, get_location_name_from_address + class Client: + version: typing.List[int] = [0, 0, 0] + tags: typing.List[str] = [] + def __init__(self, socket): self.socket = socket self.auth = False @@ -28,9 +33,12 @@ class Client: self.team = None self.slot = None self.send_index = 0 + self.tags = [] + self.version = [0, 0, 0] + class Context: - def __init__(self, host:str, port:int, password:str, location_check_points:int, hint_cost:int): + def __init__(self, host: str, port: int, password: str, location_check_points: int, hint_cost: int): self.data_filename = None self.save_filename = None self.disable_save = False @@ -69,6 +77,7 @@ class Context: logging.info(f'Loaded save file with {sum([len(p) for p in received_items.values()])} received items ' f'for {len(received_items)} players') + async def send_msgs(websocket, msgs): if not websocket or not websocket.open or websocket.closed: return @@ -91,17 +100,35 @@ def notify_all(ctx : Context, text): logging.info("Notice (all): %s" % text) broadcast_all(ctx, [['Print', text]]) -def notify_team(ctx : Context, team : int, text : str): - logging.info("Notice (Team #%d): %s" % (team+1, text)) + +def notify_team(ctx: Context, team: int, text: str): + logging.info("Notice (Team #%d): %s" % (team + 1, text)) broadcast_team(ctx, team, [['Print', text]]) -def notify_client(client : Client, text : str): + +def notify_client(client: Client, text: str): if not client.auth: return - logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team+1, text)) - asyncio.create_task(send_msgs(client.socket, [['Print', text]])) + logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) + asyncio.create_task(send_msgs(client.socket, [['Print', text]])) -async def server(websocket, path, ctx : Context): + +# separated out, due to compatibilty between client's +def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]): + cmd = [["Hint", hints]] + texts = [['Print', format_hint(ctx, team, hint)] for hint in hints] + for cmd, text in texts: + logging.info("Notice (Team #%d): %s" % (team + 1, text)) + for client in ctx.clients: + if client.auth and client.team == team: + if "Berserker" in client.tags: + payload = cmd + else: + payload = texts + asyncio.create_task(send_msgs(client.socket, payload)) + + +async def server(websocket, path, ctx: Context): client = Client(websocket) ctx.clients.append(client) @@ -126,7 +153,9 @@ async def server(websocket, path, ctx : Context): async def on_client_connected(ctx : Context, client : Client): await send_msgs(client.socket, [['RoomInfo', { 'password': ctx.password is not None, - 'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth] + 'players': [(client.team, client.slot, client.name) for client in ctx.clients if client.auth], + 'tags': ['Berserker'], + 'version': "1.0.0" }]]) async def on_client_disconnected(ctx : Context, client : Client): @@ -167,7 +196,8 @@ def get_connected_players_string(ctx : Context): text += f'{c.name} ' return 'Connected players: ' + text[:-1] -def get_received_items(ctx : Context, team, player): + +def get_received_items(ctx: Context, team: int, player: int): return ctx.received_items.setdefault((team, player), []) def tuplize_received_items(items): @@ -182,12 +212,14 @@ def send_new_items(ctx : Context): asyncio.create_task(send_msgs(client.socket, [['ReceivedItems', (client.send_index, tuplize_received_items(items)[client.send_index:])]])) client.send_index = len(items) -def forfeit_player(ctx : Context, team, slot): + +def forfeit_player(ctx: Context, team, slot): all_locations = [values[0] for values in Regions.location_table.values() if type(values[0]) is int] notify_all(ctx, "%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1)) register_location_checks(ctx, team, slot, all_locations) -def register_location_checks(ctx : Context, team, slot, locations): + +def register_location_checks(ctx: Context, team, slot, locations): ctx.location_checks[team, slot] |= set(locations) found_items = False @@ -214,7 +246,8 @@ def register_location_checks(ctx : Context, team, slot, locations): if found_items: save(ctx) -def save(ctx:Context): + +def save(ctx: Context): if not ctx.disable_save: try: with open(ctx.save_filename, "wb") as f: @@ -223,21 +256,29 @@ def save(ctx:Context): except Exception as e: logging.exception(e) -def collect_hints(ctx:Context, team, slot, item:str) -> list: + +def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[Utils.Hint]: hints = [] - seeked_item_id = Items.item_table[item][3] + seeked_item_id = Items.lookup_lower_name_to_id[item] for check, result in ctx.locations.items(): item_id, receiving_player = result if receiving_player == slot and item_id == seeked_item_id: location_id, finding_player = check found = location_id in ctx.location_checks[team, finding_player] - hinttext = f"[Hint]: {ctx.player_names[(team, slot)]}'s {item} can be found at " \ - f"{get_location_name_from_address(location_id)} in {ctx.player_names[team, finding_player]}'s World." - hints.append((found, hinttext + (" (found)" if found else ""))) + hints.append(Utils.Hint(receiving_player, finding_player, location_id, item_id, found)) return hints -async def process_client_cmd(ctx : Context, client : Client, cmd, args): + +def format_hint(ctx: Context, team: int, hint: Utils.Hint) -> str: + return f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ + f"{Items.lookup_id_to_name[hint.item]} can be found " \ + f"at {get_location_name_from_address(hint.location)} " \ + f"in {ctx.player_names[team, hint.finding_player]}'s World." \ + + (" (found)" if hint.found else "") + + +async def process_client_cmd(ctx: Context, client: Client, cmd, args): if type(cmd) is not str: await send_msgs(client.socket, [['InvalidCmd']]) return @@ -268,7 +309,10 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): await send_msgs(client.socket, [['ConnectionRefused', list(errors)]]) else: client.auth = True - reply = [['Connected', [(client.team, client.slot), [(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]] + client.version = args.get('version', Client.version) + client.tags = args.get('tags', Client.tags) + reply = [['Connected', [(client.team, client.slot), + [(p, n) for (t, p), n in ctx.player_names.items() if t == client.team]]]] items = get_received_items(ctx, client.team, client.slot) if items: reply.append(['ReceivedItems', (0, tuplize_received_items(items))]) @@ -331,20 +375,20 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): timer = 10 asyncio.create_task(countdown(ctx, timer)) elif args.startswith("!hint"): - points_available = ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - ctx.hint_cost*ctx.hints_used[client.team, client.slot] - itemname = args[6:] - if not itemname: - notify_client(client, "Use !hint {itemname}, for example !hint Lamp. " + points_available = ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - \ + ctx.hint_cost * ctx.hints_used[client.team, client.slot] + item_name = args[6:].lower() + if not item_name: + notify_client(client, "Use !hint {item_name}, for example !hint Lamp. " f"A hint costs {ctx.hint_cost} points. " f"You have {points_available} points.") - elif itemname in Items.item_table: - hints = collect_hints(ctx, client.team, client.slot, itemname) + elif item_name in Items.lookup_lower_name_to_id: + hints = collect_hints(ctx, client.team, client.slot, item_name) found = 0 - for already_found, hint in hints: - found += 1 - already_found + for hint in hints: + found += 1 - hint.found if not found: - for already_found, hint in hints: - notify_team(ctx, client.team, hint) + notify_hints(ctx, client.team, format_hint(ctx, client.team, hints)) notify_client(client, "No new items found, points refunded.") else: if ctx.hint_cost: @@ -354,15 +398,14 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args): if can_pay: ctx.hints_used[client.team, client.slot] += found - for already_found, hint in hints: - notify_team(ctx, client.team, hint) + notify_hints(ctx, client.team, format_hint(ctx, client.team, hints)) save(ctx) else: notify_client(client, f"You can't afford the hint. " - f"You have {points_available} points and need at least {ctx.hint_cost}, " - f"more if multiple items are still be found.") + f"You have {points_available} points and need at least {ctx.hint_cost}, " + f"more if multiple items are still to be found.") else: - notify_client(client, f'Item "{itemname}" not found.') + notify_client(client, f'Item "{item_name}" not found.') def set_password(ctx : Context, password): ctx.password = password @@ -407,10 +450,11 @@ async def console(ctx : Context): forfeit_player(ctx, team, slot) if command[0] == '/senditem' and len(command) > 2: [(player, item)] = re.findall(r'\S* (\S*) (.*)', input) - if item in Items.item_table: + item = item.lower() + if item in Items.lookup_lower_name_to_id: for client in ctx.clients: - if client.auth and client.name.lower() == player.lower(): - new_item = ReceivedItem(Items.item_table[item][3], "cheat console", client.slot) + if client.name.lower() == player.lower(): + new_item = ReceivedItem(Items.lookup_lower_name_to_name[item], "cheat console", client.slot) get_received_items(ctx, client.team, client.slot).append(new_item) notify_all(ctx, 'Cheat console: sending "' + item + '" to ' + client.name) send_new_items(ctx) @@ -421,11 +465,10 @@ async def console(ctx : Context): if len(command) == 1: logging.info("Use /hint {Playername} {itemname}\nFor example /hint Berserker Lamp") elif name.lower() == command[1].lower(): - item = " ".join(command[2:]) - if item in Items.item_table: + item = " ".join(command[2:]).lower() + if item in Items.lookup_lower_name_to_id: hints = collect_hints(ctx, team, slot, item) - for already_found, hint in hints: - notify_team(ctx, team, hint) + notify_hints(ctx, team, hints) else: logging.warning("Unknown item: " + item) if command[0][0] != '/': diff --git a/Utils.py b/Utils.py index e04c51ad..58d9ee1d 100644 --- a/Utils.py +++ b/Utils.py @@ -2,15 +2,27 @@ import os import re import subprocess import sys +import typing +import functools + +from yaml import load + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + def int16_as_bytes(value): value = value & 0xFFFF return [value & 0xFF, (value >> 8) & 0xFF] + def int32_as_bytes(value): value = value & 0xFFFFFFFF return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF] + def pc_to_snes(value): return ((value<<1) & 0x7F0000)|(value & 0x7FFF)|0x8000 @@ -122,16 +134,19 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap if offset - 1 in out_data: 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=(",", ":")) + json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":")) basemd5 = hashlib.md5() basemd5.update(new_rom_data) return "New Rom Hash: " + basemd5.hexdigest() -from yaml import load -import functools -try: from yaml import CLoader as Loader -except ImportError: from yaml import Loader +parse_yaml = functools.partial(load, Loader=Loader) -parse_yaml = functools.partial(load, Loader=Loader) \ No newline at end of file + +class Hint(typing.NamedTuple): + receiving_player: int + finding_player: int + location: int + item: int + found: bool