attach command to args dict
This commit is contained in:
parent
8a395a3021
commit
cbbdb2948d
|
@ -79,7 +79,7 @@ class Context():
|
||||||
self.server_task = None
|
self.server_task = None
|
||||||
self.server: typing.Optional[Endpoint] = None
|
self.server: typing.Optional[Endpoint] = None
|
||||||
self.password = password
|
self.password = password
|
||||||
self.server_version = (0, 0, 0)
|
self.server_version = Version(0, 0, 0)
|
||||||
|
|
||||||
self.team = None
|
self.team = None
|
||||||
self.slot = None
|
self.slot = None
|
||||||
|
@ -118,9 +118,9 @@ class Context():
|
||||||
async def send_msgs(self, msgs):
|
async def send_msgs(self, msgs):
|
||||||
if not self.server or not self.server.socket.open or self.server.socket.closed:
|
if not self.server or not self.server.socket.open or self.server.socket.closed:
|
||||||
return
|
return
|
||||||
await self.server.socket.send(dumps(msgs))
|
await self.server.socket.send(encode(msgs))
|
||||||
|
|
||||||
def consume_players_package(self, package:typing.List[tuple]):
|
def consume_players_package(self, package: typing.List[tuple]):
|
||||||
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,6 +131,7 @@ def color_item(item_id: int, green: bool = False) -> str:
|
||||||
item_colors.append("white_bg")
|
item_colors.append("white_bg")
|
||||||
return color(item_name, *item_colors)
|
return color(item_name, *item_colors)
|
||||||
|
|
||||||
|
|
||||||
START_RECONNECT_DELAY = 5
|
START_RECONNECT_DELAY = 5
|
||||||
SNES_RECONNECT_DELAY = 5
|
SNES_RECONNECT_DELAY = 5
|
||||||
SERVER_RECONNECT_DELAY = 5
|
SERVER_RECONNECT_DELAY = 5
|
||||||
|
@ -160,8 +161,8 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
||||||
|
|
||||||
|
location_shop_order = [name for name, info in
|
||||||
location_shop_order = [name for name, info in Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order
|
Shops.shop_table.items()] # probably don't leave this here. This relies on python 3.6+ dictionary keys having defined order
|
||||||
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||||
|
|
||||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||||
|
@ -711,12 +712,6 @@ async def snes_flush_writes(ctx: Context):
|
||||||
await snes_write(ctx, writes)
|
await snes_write(ctx, writes)
|
||||||
|
|
||||||
|
|
||||||
async def send_msgs(websocket, msgs):
|
|
||||||
if not websocket or not websocket.open or websocket.closed:
|
|
||||||
return
|
|
||||||
await websocket.send(dumps(msgs))
|
|
||||||
|
|
||||||
|
|
||||||
async def server_loop(ctx: Context, address=None):
|
async def server_loop(ctx: Context, address=None):
|
||||||
global SERVER_RECONNECT_DELAY
|
global SERVER_RECONNECT_DELAY
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
|
@ -754,8 +749,8 @@ async def server_loop(ctx: Context, address=None):
|
||||||
ctx.ui_node.send_connection_status(ctx)
|
ctx.ui_node.send_connection_status(ctx)
|
||||||
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
|
SERVER_RECONNECT_DELAY = START_RECONNECT_DELAY
|
||||||
async for data in ctx.server.socket:
|
async for data in ctx.server.socket:
|
||||||
for msg in loads(data):
|
for msg in decode(data):
|
||||||
await process_server_cmd(ctx, msg[0], msg[1])
|
await process_server_cmd(ctx, msg)
|
||||||
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
|
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
|
||||||
except WebUI.WaitingForUiException:
|
except WebUI.WaitingForUiException:
|
||||||
pass
|
pass
|
||||||
|
@ -776,7 +771,7 @@ async def server_loop(ctx: Context, address=None):
|
||||||
ctx.auth = None
|
ctx.auth = None
|
||||||
ctx.items_received = []
|
ctx.items_received = []
|
||||||
ctx.locations_info = {}
|
ctx.locations_info = {}
|
||||||
ctx.server_version = (0, 0, 0)
|
ctx.server_version = Version(0, 0, 0)
|
||||||
if ctx.server and ctx.server.socket is not None:
|
if ctx.server and ctx.server.socket is not None:
|
||||||
await ctx.server.socket.close()
|
await ctx.server.socket.close()
|
||||||
ctx.server = None
|
ctx.server = None
|
||||||
|
@ -799,6 +794,8 @@ async def server_autoreconnect(ctx: Context):
|
||||||
|
|
||||||
|
|
||||||
missing_unknown = re.compile("Unknown Location ID: (?P<ID>\d+)")
|
missing_unknown = re.compile("Unknown Location ID: (?P<ID>\d+)")
|
||||||
|
|
||||||
|
|
||||||
def convert_unknown_missing(missing_items: list) -> list:
|
def convert_unknown_missing(missing_items: list) -> list:
|
||||||
missing = []
|
missing = []
|
||||||
for location in missing_items:
|
for location in missing_items:
|
||||||
|
@ -810,7 +807,8 @@ def convert_unknown_missing(missing_items: list) -> list:
|
||||||
return missing
|
return missing
|
||||||
|
|
||||||
|
|
||||||
async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]):
|
async def process_server_cmd(ctx: Context, args: dict):
|
||||||
|
cmd = args["cmd"]
|
||||||
if cmd == 'RoomInfo':
|
if cmd == 'RoomInfo':
|
||||||
logger.info('--------------------------------')
|
logger.info('--------------------------------')
|
||||||
logger.info('Room Information:')
|
logger.info('Room Information:')
|
||||||
|
@ -873,10 +871,11 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
|
||||||
ctx.consume_players_package(args["players"])
|
ctx.consume_players_package(args["players"])
|
||||||
msgs = []
|
msgs = []
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
msgs.append(['LocationChecks',
|
msgs.append({"cmd": "LocationChecks",
|
||||||
{"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}])
|
"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]})
|
||||||
if ctx.locations_scouted:
|
if ctx.locations_scouted:
|
||||||
msgs.append(['LocationScouts', {"locations": list(ctx.locations_scouted)}])
|
msgs.append({"cmd": "LocationScouts",
|
||||||
|
"locations": list(ctx.locations_scouted)})
|
||||||
if msgs:
|
if msgs:
|
||||||
await ctx.send_msgs(msgs)
|
await ctx.send_msgs(msgs)
|
||||||
if ctx.finished_game:
|
if ctx.finished_game:
|
||||||
|
@ -895,13 +894,12 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
|
||||||
if start_index == 0:
|
if start_index == 0:
|
||||||
ctx.items_received = []
|
ctx.items_received = []
|
||||||
elif start_index != len(ctx.items_received):
|
elif start_index != len(ctx.items_received):
|
||||||
sync_msg = [['Sync', None]]
|
sync_msg = [{'cmd': 'Sync'}]
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
sync_msg.append(['LocationChecks',
|
sync_msg.append({"cmd": "LocationChecks",
|
||||||
{"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]}])
|
"locations": [Regions.lookup_name_to_id[loc] for loc in ctx.locations_checked]})
|
||||||
await ctx.send_msgs(sync_msg)
|
await ctx.send_msgs(sync_msg)
|
||||||
if start_index == len(ctx.items_received):
|
if start_index == len(ctx.items_received):
|
||||||
|
|
||||||
for item in args['items']:
|
for item in args['items']:
|
||||||
ctx.items_received.append(NetworkItem(*item))
|
ctx.items_received.append(NetworkItem(*item))
|
||||||
ctx.watcher_event.set()
|
ctx.watcher_event.set()
|
||||||
|
@ -916,7 +914,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
|
||||||
ctx.locations_info[location] = (item, player)
|
ctx.locations_info[location] = (item, player)
|
||||||
ctx.watcher_event.set()
|
ctx.watcher_event.set()
|
||||||
|
|
||||||
elif cmd == 'ItemSent': # going away
|
elif cmd == 'ItemSent': # going away
|
||||||
found = NetworkItem(*args["item"])
|
found = NetworkItem(*args["item"])
|
||||||
receiving_player = args["receiver"]
|
receiving_player = args["receiver"]
|
||||||
ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player],
|
ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player],
|
||||||
|
@ -925,12 +923,13 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
|
||||||
get_item_name_from_id(found.item) in Items.progression_items)
|
get_item_name_from_id(found.item) in Items.progression_items)
|
||||||
item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
|
item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
|
||||||
found_player = color(ctx.player_names[found.player], 'yellow' if found.player != ctx.slot else 'magenta')
|
found_player = color(ctx.player_names[found.player], 'yellow' if found.player != ctx.slot else 'magenta')
|
||||||
receiving_player = color(ctx.player_names[receiving_player], 'yellow' if receiving_player != ctx.slot else 'magenta')
|
receiving_player = color(ctx.player_names[receiving_player],
|
||||||
|
'yellow' if receiving_player != ctx.slot else 'magenta')
|
||||||
logging.info(
|
logging.info(
|
||||||
'%s sent %s to %s (%s)' % (found_player, item, receiving_player,
|
'%s sent %s to %s (%s)' % (found_player, item, receiving_player,
|
||||||
color(get_location_name_from_address(found.location), 'blue_bg', 'white')))
|
color(get_location_name_from_address(found.location), 'blue_bg', 'white')))
|
||||||
|
|
||||||
elif cmd == 'ItemFound': # going away
|
elif cmd == 'ItemFound': # going away
|
||||||
found = NetworkItem(*args["item"])
|
found = NetworkItem(*args["item"])
|
||||||
ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item),
|
ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item),
|
||||||
get_location_name_from_address(found.location), found.player == ctx.slot,
|
get_location_name_from_address(found.location), found.player == ctx.slot,
|
||||||
|
@ -940,7 +939,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
|
||||||
logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location),
|
logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location),
|
||||||
'blue_bg', 'white')))
|
'blue_bg', 'white')))
|
||||||
|
|
||||||
elif cmd == 'Hint': # going away
|
elif cmd == 'Hint': # going away
|
||||||
hints = [Utils.Hint(*hint) for hint in args["hints"]]
|
hints = [Utils.Hint(*hint) for hint in args["hints"]]
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player],
|
ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player],
|
||||||
|
@ -998,10 +997,11 @@ async def server_auth(ctx: Context, password_requested):
|
||||||
ctx.awaiting_rom = False
|
ctx.awaiting_rom = False
|
||||||
ctx.auth = ctx.rom
|
ctx.auth = ctx.rom
|
||||||
auth = base64.b64encode(ctx.rom).decode()
|
auth = base64.b64encode(ctx.rom).decode()
|
||||||
await ctx.send_msgs([['Connect', {
|
await ctx.send_msgs([{"cmd": 'Connect',
|
||||||
'password': ctx.password, 'name': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx),
|
'password': ctx.password, 'name': auth, 'version': Utils._version_tuple,
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
'tags': get_tags(ctx),
|
||||||
}]])
|
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
async def console_input(ctx: Context):
|
async def console_input(ctx: Context):
|
||||||
|
@ -1094,16 +1094,6 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
self.output("No missing location checks found.")
|
self.output("No missing location checks found.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _cmd_show_items(self, toggle: str = "") -> bool:
|
|
||||||
"""Toggle showing of items received across the team"""
|
|
||||||
if toggle:
|
|
||||||
self.ctx.found_items = toggle.lower() in {"1", "true", "on"}
|
|
||||||
else:
|
|
||||||
self.ctx.found_items = not self.ctx.found_items
|
|
||||||
logger.info(f"Set showing team items to {self.ctx.found_items}")
|
|
||||||
asyncio.create_task(self.ctx.send_msgs([['UpdateTags', get_tags(self.ctx)]]))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _cmd_slow_mode(self, toggle: str = ""):
|
def _cmd_slow_mode(self, toggle: str = ""):
|
||||||
"""Toggle slow mode, which limits how fast you send / receive items."""
|
"""Toggle slow mode, which limits how fast you send / receive items."""
|
||||||
if toggle:
|
if toggle:
|
||||||
|
@ -1120,7 +1110,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||||
self.output("Web UI was never started.")
|
self.output("Web UI was never started.")
|
||||||
|
|
||||||
def default(self, raw: str):
|
def default(self, raw: str):
|
||||||
asyncio.create_task(self.ctx.send_msgs([['Say', {"text": raw}]]))
|
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
|
||||||
|
|
||||||
|
|
||||||
async def console_loop(ctx: Context):
|
async def console_loop(ctx: Context):
|
||||||
|
@ -1165,7 +1155,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if roomid in location_shop_ids:
|
if roomid in location_shop_ids:
|
||||||
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order)*3)+5)
|
misc_data = await snes_read(ctx, SHOP_ADDR, (len(location_shop_order) * 3) + 5)
|
||||||
for cnt, b in enumerate(misc_data):
|
for cnt, b in enumerate(misc_data):
|
||||||
my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt]
|
my_check = Shops.shop_table_by_location_id[Shops.SHOP_ID_START + cnt]
|
||||||
if int(b) > 0 and my_check not in ctx.locations_checked:
|
if int(b) > 0 and my_check not in ctx.locations_checked:
|
||||||
|
@ -1174,7 +1164,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||||
print(e)
|
print(e)
|
||||||
logger.info(f"Exception: {e}")
|
logger.info(f"Exception: {e}")
|
||||||
|
|
||||||
|
|
||||||
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
for location, (loc_roomid, loc_mask) in location_table_uw.items():
|
||||||
try:
|
try:
|
||||||
if location not in ctx.locations_checked and loc_roomid == roomid and (
|
if location not in ctx.locations_checked and loc_roomid == roomid and (
|
||||||
|
@ -1238,14 +1227,13 @@ async def track_locations(ctx: Context, roomid, roomdata):
|
||||||
print(e)
|
print(e)
|
||||||
logger.info(f"Exception: {e}")
|
logger.info(f"Exception: {e}")
|
||||||
|
|
||||||
|
|
||||||
if new_locations:
|
if new_locations:
|
||||||
await ctx.send_msgs([['LocationChecks', {"locations": new_locations}]])
|
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||||
|
|
||||||
|
|
||||||
async def send_finished_game(ctx: Context):
|
async def send_finished_game(ctx: Context):
|
||||||
try:
|
try:
|
||||||
await ctx.send_msgs([['StatusUpdate', {"status": CLientStatus.CLIENT_GOAL}]])
|
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": CLientStatus.CLIENT_GOAL}])
|
||||||
ctx.finished_game = True
|
ctx.finished_game = True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
|
@ -1343,7 +1331,7 @@ async def game_watcher(ctx: Context):
|
||||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||||
ctx.locations_scouted.add(scout_location)
|
ctx.locations_scouted.add(scout_location)
|
||||||
logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
logger.info(f'Scouting item at {list(Regions.location_table.keys())[scout_location - 1]}')
|
||||||
await ctx.send_msgs([['LocationScouts', {"locations": [scout_location]}]])
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||||
await track_locations(ctx, roomid, roomdata)
|
await track_locations(ctx, roomid, roomdata)
|
||||||
|
|
||||||
|
|
||||||
|
|
101
MultiServer.py
101
MultiServer.py
|
@ -28,15 +28,15 @@ from fuzzywuzzy import process as fuzzy_process
|
||||||
from worlds.alttp import Items, Regions
|
from worlds.alttp import Items, Regions
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
from Utils import get_item_name_from_id, get_location_name_from_address, \
|
||||||
_version_tuple, restricted_loads
|
_version_tuple, restricted_loads, Version
|
||||||
from NetUtils import Node, Endpoint, CLientStatus, NetworkItem
|
from NetUtils import Node, Endpoint, CLientStatus, NetworkItem, decode
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
|
console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
|
||||||
|
|
||||||
|
|
||||||
class Client(Endpoint):
|
class Client(Endpoint):
|
||||||
version: typing.List[int] = [0, 0, 0]
|
version = Version(0, 0, 0)
|
||||||
tags: typing.List[str] = []
|
tags: typing.List[str] = []
|
||||||
|
|
||||||
def __init__(self, socket: websockets.server.WebSocketServerProtocol, ctx: Context):
|
def __init__(self, socket: websockets.server.WebSocketServerProtocol, ctx: Context):
|
||||||
|
@ -47,7 +47,6 @@ class Client(Endpoint):
|
||||||
self.slot = None
|
self.slot = None
|
||||||
self.send_index = 0
|
self.send_index = 0
|
||||||
self.tags = []
|
self.tags = []
|
||||||
self.version = [0, 0, 0]
|
|
||||||
self.messageprocessor = client_message_processor(ctx, self)
|
self.messageprocessor = client_message_processor(ctx, self)
|
||||||
self.ctx = weakref.ref(ctx)
|
self.ctx = weakref.ref(ctx)
|
||||||
|
|
||||||
|
@ -302,18 +301,18 @@ class Context(Node):
|
||||||
|
|
||||||
def notify_all(self, text):
|
def notify_all(self, text):
|
||||||
logging.info("Notice (all): %s" % text)
|
logging.info("Notice (all): %s" % text)
|
||||||
self.broadcast_all([['Print', {"text": text}]])
|
self.broadcast_all([{"cmd": "Print", "text": text}])
|
||||||
|
|
||||||
def notify_client(self, client: Client, text: str):
|
def notify_client(self, client: Client, text: str):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
|
||||||
asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}]]))
|
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
|
||||||
|
|
||||||
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
|
||||||
if not client.auth:
|
if not client.auth:
|
||||||
return
|
return
|
||||||
asyncio.create_task(self.send_msgs(client, [['Print', {"text": text}] for text in texts]))
|
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
|
||||||
|
|
||||||
def broadcast_team(self, team, msgs):
|
def broadcast_team(self, team, msgs):
|
||||||
for client in self.endpoints:
|
for client in self.endpoints:
|
||||||
|
@ -321,7 +320,7 @@ class Context(Node):
|
||||||
asyncio.create_task(self.send_msgs(client, msgs))
|
asyncio.create_task(self.send_msgs(client, msgs))
|
||||||
|
|
||||||
def broadcast_all(self, msgs):
|
def broadcast_all(self, msgs):
|
||||||
msgs = dumps(msgs)
|
msgs = self.dumper(msgs)
|
||||||
for endpoint in self.endpoints:
|
for endpoint in self.endpoints:
|
||||||
if endpoint.auth:
|
if endpoint.auth:
|
||||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||||
|
@ -333,9 +332,9 @@ class Context(Node):
|
||||||
|
|
||||||
# separated out, due to compatibilty between clients
|
# separated out, due to compatibilty between clients
|
||||||
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
|
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
|
||||||
cmd = dumps([["Hint", {"hints" : hints}]])
|
cmd = ctx.dumper([{"cmd": "Hint", "hints" : hints}])
|
||||||
texts = [['PrintHTML', format_hint(ctx, team, hint)] for hint in hints]
|
texts = ([format_hint(ctx, team, hint)] for hint in hints)
|
||||||
for _, text in texts:
|
for text in texts:
|
||||||
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
logging.info("Notice (Team #%d): %s" % (team + 1, text))
|
||||||
for client in ctx.endpoints:
|
for client in ctx.endpoints:
|
||||||
if client.auth and client.team == team:
|
if client.auth and client.team == team:
|
||||||
|
@ -343,8 +342,8 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
|
||||||
|
|
||||||
|
|
||||||
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
|
||||||
cmd = dumps([["RoomUpdate",
|
cmd = ctx.dumper([{"cmd": "RoomUpdate",
|
||||||
{"players": ctx.get_players_package()}]])
|
"players": ctx.get_players_package()}])
|
||||||
if client is None:
|
if client is None:
|
||||||
for client in ctx.endpoints:
|
for client in ctx.endpoints:
|
||||||
if client.team == team and client.auth:
|
if client.team == team and client.auth:
|
||||||
|
@ -360,8 +359,8 @@ async def server(websocket, path, ctx: Context):
|
||||||
try:
|
try:
|
||||||
await on_client_connected(ctx, client)
|
await on_client_connected(ctx, client)
|
||||||
async for data in websocket:
|
async for data in websocket:
|
||||||
for msg in loads(data):
|
for msg in decode(data):
|
||||||
await process_client_cmd(ctx, client, msg[0], msg[1])
|
await process_client_cmd(ctx, client, msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, websockets.WebSocketException):
|
if not isinstance(e, websockets.WebSocketException):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
|
@ -372,7 +371,8 @@ async def server(websocket, path, ctx: Context):
|
||||||
|
|
||||||
|
|
||||||
async def on_client_connected(ctx: Context, client: Client):
|
async def on_client_connected(ctx: Context, client: Client):
|
||||||
await ctx.send_msgs(client, [['RoomInfo', {
|
await ctx.send_msgs(client, [{
|
||||||
|
'cmd': 'RoomInfo',
|
||||||
'password': ctx.password is not None,
|
'password': ctx.password is not None,
|
||||||
'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client
|
'players': [(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name)) for client
|
||||||
in ctx.endpoints if client.auth],
|
in ctx.endpoints if client.auth],
|
||||||
|
@ -384,7 +384,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||||
'remaining_mode': ctx.remaining_mode,
|
'remaining_mode': ctx.remaining_mode,
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points
|
'location_check_points': ctx.location_check_points
|
||||||
}]])
|
}])
|
||||||
|
|
||||||
|
|
||||||
async def on_client_disconnected(ctx: Context, client: Client):
|
async def on_client_disconnected(ctx: Context, client: Client):
|
||||||
|
@ -420,12 +420,6 @@ async def countdown(ctx: Context, timer):
|
||||||
ctx.notify_all(f'[Server]: GO')
|
ctx.notify_all(f'[Server]: GO')
|
||||||
ctx.countdown_timer = 0
|
ctx.countdown_timer = 0
|
||||||
|
|
||||||
async def missing(ctx: Context, client: Client, locations: list, checked_locations: list):
|
|
||||||
await ctx.send_msgs(client, [['Missing', {
|
|
||||||
'locations': dumps(locations),
|
|
||||||
'checked_locations': dumps(checked_locations)
|
|
||||||
}]])
|
|
||||||
|
|
||||||
|
|
||||||
def get_players_string(ctx: Context):
|
def get_players_string(ctx: Context):
|
||||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||||
|
@ -459,9 +453,10 @@ def send_new_items(ctx: Context):
|
||||||
continue
|
continue
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if len(items) > client.send_index:
|
if len(items) > client.send_index:
|
||||||
asyncio.create_task(ctx.send_msgs(client, [
|
asyncio.create_task(ctx.send_msgs(client, [{
|
||||||
['ReceivedItems', {"index": client.send_index,
|
"cmd": "ReceivedItems",
|
||||||
"items": tuplize_received_items(items)[client.send_index:]}]]))
|
"index": client.send_index,
|
||||||
|
"items": tuplize_received_items(items)[client.send_index:]}]))
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
|
|
||||||
|
@ -514,15 +509,16 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||||
for client in ctx.endpoints:
|
for client in ctx.endpoints:
|
||||||
if client.team == team and client.wants_item_notification:
|
if client.team == team and client.wants_item_notification:
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
ctx.send_msgs(client, [['ItemFound',
|
ctx.send_msgs(client, [{"cmd": "ItemFound",
|
||||||
{"item": NetworkItem(target_item, location, slot)}]]))
|
"item": NetworkItem(target_item, location, slot)}]))
|
||||||
ctx.location_checks[team, slot] |= known_locations
|
ctx.location_checks[team, slot] |= known_locations
|
||||||
send_new_items(ctx)
|
send_new_items(ctx)
|
||||||
|
|
||||||
if found_items:
|
if found_items:
|
||||||
for client in ctx.endpoints:
|
for client in ctx.endpoints:
|
||||||
if client.team == team and client.slot == slot:
|
if client.team == team and client.slot == slot:
|
||||||
asyncio.create_task(ctx.send_msgs(client, [["RoomUpdate", {"hint_points": get_client_points(ctx, client)}]]))
|
asyncio.create_task(ctx.send_msgs(client, [{"cmd": "RoomUpdate",
|
||||||
|
"hint_points": get_client_points(ctx, client)}]))
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -792,9 +788,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
self.output(
|
self.output(
|
||||||
"Sorry, client forfeiting requires you to have beaten the game on this server."
|
"Sorry, client forfeiting requires you to have beaten the game on this server."
|
||||||
" You can ask the server admin for a /forfeit")
|
" You can ask the server admin for a /forfeit")
|
||||||
if self.client.version < [2, 1, 0]:
|
|
||||||
self.output(
|
|
||||||
"Your client is too old to send game beaten information. Please update, load you savegame and reconnect.")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_remaining(self) -> bool:
|
def _cmd_remaining(self) -> bool:
|
||||||
|
@ -823,9 +816,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||||
else:
|
else:
|
||||||
self.output(
|
self.output(
|
||||||
"Sorry, !remaining requires you to have beaten the game on this server")
|
"Sorry, !remaining requires you to have beaten the game on this server")
|
||||||
if self.client.version < [2, 1, 0]:
|
|
||||||
self.output(
|
|
||||||
"Your client is too old to send game beaten information. Please update, load you savegame and reconnect.")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -978,20 +968,22 @@ def get_client_points(ctx: Context, client: Client) -> int:
|
||||||
ctx.hint_cost * ctx.hints_used[client.team, client.slot])
|
ctx.hint_cost * ctx.hints_used[client.team, client.slot])
|
||||||
|
|
||||||
|
|
||||||
async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typing.Optional[dict]):
|
async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||||
|
cmd: str = args["cmd"]
|
||||||
|
|
||||||
if type(cmd) is not str:
|
if type(cmd) is not str:
|
||||||
await ctx.send_msgs(client, [['InvalidCmd', {"text": f"Command should be str, got {type(cmd)}"}]])
|
await ctx.send_msgs(client, [{"cmd": "InvalidCmd", "text": f"Command should be str, got {type(cmd)}"}])
|
||||||
return
|
return
|
||||||
|
|
||||||
if args is not None and type(args) != dict:
|
if args is not None and type(args) != dict:
|
||||||
await ctx.send_msgs(client, [['InvalidArguments',
|
await ctx.send_msgs(client, [{"cmd": "InvalidArguments",
|
||||||
{'text': f'Expected Optional[dict], got {type(args)} for {cmd}'}]])
|
'text': f'Expected Optional[dict], got {type(args)} for {cmd}'}])
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd == 'Connect':
|
if cmd == 'Connect':
|
||||||
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
|
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
|
||||||
'game' not in args:
|
'game' not in args:
|
||||||
await ctx.send_msgs(client, [['InvalidArguments', {'text': 'Connect'}]])
|
await ctx.send_msgs(client, [{'cmd': 'InvalidArguments', 'text': 'Connect'}])
|
||||||
return
|
return
|
||||||
|
|
||||||
errors = set()
|
errors = set()
|
||||||
|
@ -1019,29 +1011,31 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
|
||||||
client.team = team
|
client.team = team
|
||||||
client.slot = slot
|
client.slot = slot
|
||||||
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
|
minver = Utils.Version(*(ctx.minimum_client_versions.get((team, slot), (0,0,0))))
|
||||||
if minver > tuple(args['version']):
|
if minver > args['version']:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
|
|
||||||
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
if ctx.compatibility == 1 and "AP" not in args['tags']:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
#only exact version match allowed
|
#only exact version match allowed
|
||||||
elif ctx.compatibility == 0 and tuple(args['version']) != _version_tuple:
|
elif ctx.compatibility == 0 and args['version'] != _version_tuple:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if errors:
|
if errors:
|
||||||
logging.info(f"A client connection was refused due to: {errors}")
|
logging.info(f"A client connection was refused due to: {errors}")
|
||||||
await ctx.send_msgs(client, [['ConnectionRefused', {"errors": list(errors)}]])
|
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
|
||||||
else:
|
else:
|
||||||
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
ctx.client_ids[client.team, client.slot] = args["uuid"]
|
||||||
client.auth = True
|
client.auth = True
|
||||||
client.version = args['version']
|
client.version = args['version']
|
||||||
client.tags = args['tags']
|
client.tags = args['tags']
|
||||||
reply = [['Connected', {"team": client.team, "slot": client.slot,
|
reply = [{
|
||||||
"players": ctx.get_players_package(),
|
"cmd": "Connected",
|
||||||
"missing_checks": get_missing_checks(ctx, client),
|
"team": client.team, "slot": client.slot,
|
||||||
"items_checked": get_checked_checks(ctx, client)}]]
|
"players": ctx.get_players_package(),
|
||||||
|
"missing_checks": get_missing_checks(ctx, client),
|
||||||
|
"items_checked": get_checked_checks(ctx, client)}]
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
reply.append(['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}])
|
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": tuplize_received_items(items)})
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
|
|
||||||
await ctx.send_msgs(client, reply)
|
await ctx.send_msgs(client, reply)
|
||||||
|
@ -1052,7 +1046,8 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
|
||||||
items = get_received_items(ctx, client.team, client.slot)
|
items = get_received_items(ctx, client.team, client.slot)
|
||||||
if items:
|
if items:
|
||||||
client.send_index = len(items)
|
client.send_index = len(items)
|
||||||
await ctx.send_msgs(client, [['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}]])
|
await ctx.send_msgs(client, [{"cmd": "ReceivedItems","index": 0,
|
||||||
|
"items": tuplize_received_items(items)}])
|
||||||
|
|
||||||
elif cmd == 'LocationChecks':
|
elif cmd == 'LocationChecks':
|
||||||
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
register_location_checks(ctx, client.team, client.slot, args["locations"])
|
||||||
|
@ -1061,7 +1056,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
|
||||||
locs = []
|
locs = []
|
||||||
for location in args["locations"]:
|
for location in args["locations"]:
|
||||||
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
if type(location) is not int or 0 >= location > len(Regions.location_table):
|
||||||
await ctx.send_msgs(client, [['InvalidArguments', {"text": 'LocationScouts'}]])
|
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'LocationScouts'}])
|
||||||
return
|
return
|
||||||
loc_name = list(Regions.location_table.keys())[location - 1]
|
loc_name = list(Regions.location_table.keys())[location - 1]
|
||||||
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
target_item, target_player = ctx.locations[(Regions.location_table[loc_name][0], client.slot)]
|
||||||
|
@ -1074,11 +1069,11 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
|
||||||
locs.append([target_item, location, target_player])
|
locs.append([target_item, location, target_player])
|
||||||
|
|
||||||
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
# logging.info(f"{client.name} in team {client.team+1} scouted {', '.join([l[0] for l in locs])}")
|
||||||
await ctx.send_msgs(client, [['LocationInfo', {'locations': locs}]])
|
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||||
|
|
||||||
elif cmd == 'UpdateTags':
|
elif cmd == 'UpdateTags':
|
||||||
if not args or type(args) is not list:
|
if not args or type(args) is not list:
|
||||||
await ctx.send_msgs(client, [['InvalidArguments', {"text": 'UpdateTags'}]])
|
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text": 'UpdateTags'}])
|
||||||
return
|
return
|
||||||
client.tags = args
|
client.tags = args
|
||||||
|
|
||||||
|
@ -1095,7 +1090,7 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
|
||||||
|
|
||||||
if cmd == 'Say':
|
if cmd == 'Say':
|
||||||
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
|
||||||
await ctx.send_msgs(client, [['InvalidArguments', {"text" : 'Say'}]])
|
await ctx.send_msgs(client, [{"cmd": "InvalidArguments", "text" : 'Say'}])
|
||||||
return
|
return
|
||||||
|
|
||||||
client.messageprocessor(args["text"])
|
client.messageprocessor(args["text"])
|
||||||
|
|
56
NetUtils.py
56
NetUtils.py
|
@ -3,19 +3,58 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import enum
|
import enum
|
||||||
from json import loads, dumps
|
from json import JSONEncoder, JSONDecoder
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
|
|
||||||
class JSONMessagePart(typing.TypedDict):
|
class JSONMessagePart(typing.TypedDict):
|
||||||
type: typing.Optional[str]
|
type: typing.Optional[str]
|
||||||
color: typing.Optional[str]
|
color: typing.Optional[str]
|
||||||
text: typing.Optional[str]
|
text: typing.Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
|
||||||
|
if isinstance(obj, tuple) and hasattr(obj, "_fields"): # NamedTuple is not actually a parent class
|
||||||
|
data = obj._asdict()
|
||||||
|
data["class"] = obj.__class__.__name__
|
||||||
|
return data
|
||||||
|
if isinstance(obj, (tuple, list)):
|
||||||
|
return tuple(_scan_for_TypedTuples(o) for o in obj)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
_encode = JSONEncoder(
|
||||||
|
ensure_ascii=False,
|
||||||
|
check_circular=False,
|
||||||
|
).encode
|
||||||
|
|
||||||
|
|
||||||
|
def encode(obj):
|
||||||
|
return _encode(_scan_for_TypedTuples(obj))
|
||||||
|
|
||||||
|
from Utils import Version # for object hook
|
||||||
|
whitelist = {"NetworkPlayer", "NetworkItem", "Version"}
|
||||||
|
|
||||||
|
def _object_hook(o: typing.Any) -> typing.Any:
|
||||||
|
if isinstance(o, dict):
|
||||||
|
cls = o.get("class", None)
|
||||||
|
if cls in whitelist:
|
||||||
|
del (o["class"])
|
||||||
|
return globals()[cls](**o)
|
||||||
|
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
decode = JSONDecoder(object_hook=_object_hook).decode
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
endpoints: typing.List
|
endpoints: typing.List
|
||||||
dumper = staticmethod(dumps)
|
dumper = staticmethod(encode)
|
||||||
loader = staticmethod(loads)
|
loader = staticmethod(decode)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.endpoints = []
|
self.endpoints = []
|
||||||
|
@ -26,13 +65,14 @@ class Node:
|
||||||
for endpoint in self.endpoints:
|
for endpoint in self.endpoints:
|
||||||
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
|
||||||
|
|
||||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[typing.Sequence[str, typing.Optional[dict]]]):
|
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]):
|
||||||
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
|
||||||
return
|
return
|
||||||
|
msg = self.dumper(msgs)
|
||||||
try:
|
try:
|
||||||
await endpoint.socket.send(self.dumper(msgs))
|
await endpoint.socket.send(msg)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
logging.exception("Exception during send_msgs")
|
logging.exception(f"Exception during send_msgs, could not send {msg}")
|
||||||
await self.disconnect(endpoint)
|
await self.disconnect(endpoint)
|
||||||
|
|
||||||
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
|
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
|
||||||
|
@ -104,7 +144,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||||
return node.get("text", "")
|
return node.get("text", "")
|
||||||
|
|
||||||
def _handle_player_id(self, node: JSONMessagePart):
|
def _handle_player_id(self, node: JSONMessagePart):
|
||||||
player = node["player"]
|
player = node["text"]
|
||||||
node["color"] = 'yellow' if player != self.ctx.slot else 'magenta'
|
node["color"] = 'yellow' if player != self.ctx.slot else 'magenta'
|
||||||
node["text"] = self.ctx.player_names[player]
|
node["text"] = self.ctx.player_names[player]
|
||||||
return self._handle_color(node)
|
return self._handle_color(node)
|
||||||
|
@ -115,7 +155,6 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||||
return self._handle_color(node)
|
return self._handle_color(node)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
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,
|
'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}
|
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||||
|
@ -136,6 +175,7 @@ class CLientStatus(enum.IntEnum):
|
||||||
CLIENT_PLAYING = 20
|
CLIENT_PLAYING = 20
|
||||||
CLIENT_GOAL = 30
|
CLIENT_GOAL = 30
|
||||||
|
|
||||||
|
|
||||||
class NetworkPlayer(typing.NamedTuple):
|
class NetworkPlayer(typing.NamedTuple):
|
||||||
team: int
|
team: int
|
||||||
slot: int
|
slot: int
|
||||||
|
|
2
Utils.py
2
Utils.py
|
@ -10,7 +10,7 @@ def tuplize_version(version: str) -> typing.Tuple[int, ...]:
|
||||||
class Version(typing.NamedTuple):
|
class Version(typing.NamedTuple):
|
||||||
major: int
|
major: int
|
||||||
minor: int
|
minor: int
|
||||||
micro: int
|
build: int
|
||||||
|
|
||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
_version_tuple = tuplize_version(__version__)
|
_version_tuple = tuplize_version(__version__)
|
||||||
|
|
Loading…
Reference in New Issue