implement server-client handshake and move hint system to optional colorama support

This commit is contained in:
Fabian Dill 2020-02-16 15:32:40 +01:00
parent 0986b36b39
commit b04db006e0
5 changed files with 275 additions and 116 deletions

View File

@ -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 = {}

141
Items.py
View File

@ -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()}

View File

@ -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):

View File

@ -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] != '/':

View File

@ -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)
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
location: int
item: int
found: bool