| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | import argparse | 
					
						
							|  |  |  | import asyncio | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | import collections | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  | import contextlib | 
					
						
							| 
									
										
										
										
											2023-11-24 17:14:07 -06:00
										 |  |  | import copy | 
					
						
							| 
									
										
										
										
											2020-04-20 04:36:56 +02:00
										 |  |  | import datetime | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  | import functools | 
					
						
							|  |  |  | import hashlib | 
					
						
							|  |  |  | import inspect | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2023-11-24 17:42:22 +01:00
										 |  |  | import math | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  | import operator | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  | import pickle | 
					
						
							|  |  |  | import random | 
					
						
							|  |  |  | import threading | 
					
						
							|  |  |  | import time | 
					
						
							|  |  |  | import typing | 
					
						
							|  |  |  | import weakref | 
					
						
							|  |  |  | import zlib | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-18 15:45:52 +01:00
										 |  |  | import ModuleUpdate | 
					
						
							| 
									
										
										
										
											2020-03-13 03:53:20 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-18 15:45:52 +01:00
										 |  |  | ModuleUpdate.update() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  | if typing.TYPE_CHECKING: | 
					
						
							|  |  |  |     import ssl | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-18 15:45:52 +01:00
										 |  |  | import websockets | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | import colorama | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  | try: | 
					
						
							|  |  |  |     # ponyorm is a requirement for webhost, not default server, so may not be importable | 
					
						
							|  |  |  |     from pony.orm.dbapiprovider import OperationalError | 
					
						
							|  |  |  | except ImportError: | 
					
						
							|  |  |  |     OperationalError = ConnectionError | 
					
						
							| 
									
										
										
										
											2021-12-03 05:24:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-07 11:37:50 +01:00
										 |  |  | import NetUtils | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2024-06-01 14:32:41 +02:00
										 |  |  | from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     SlotType, LocationStore | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  | min_client_version = Version(0, 1, 6) | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | colorama.init() | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-24 21:14:46 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | def remove_from_list(container, value): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         container.remove(value) | 
					
						
							|  |  |  |     except ValueError: | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  |     return container | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def pop_from_container(container, value): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         container.pop(value) | 
					
						
							|  |  |  |     except ValueError: | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  |     return container | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def update_dict(dictionary, entries): | 
					
						
							|  |  |  |     dictionary.update(entries) | 
					
						
							|  |  |  |     return dictionary | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  | # functions callable on storable data on the server by clients | 
					
						
							|  |  |  | modify_functions = { | 
					
						
							| 
									
										
										
										
											2023-11-24 17:42:22 +01:00
										 |  |  |     # generic: | 
					
						
							|  |  |  |     "replace": lambda old, new: new, | 
					
						
							|  |  |  |     "default": lambda old, new: old, | 
					
						
							|  |  |  |     # numeric: | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |     "add": operator.add,  # add together two objects, using python's "+" operator (works on strings and lists as append) | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |     "mul": operator.mul, | 
					
						
							| 
									
										
										
										
											2023-11-24 17:42:22 +01:00
										 |  |  |     "pow": operator.pow, | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |     "mod": operator.mod, | 
					
						
							| 
									
										
										
										
											2023-11-24 17:42:22 +01:00
										 |  |  |     "floor": lambda value, _: math.floor(value), | 
					
						
							|  |  |  |     "ceil": lambda value, _: math.ceil(value), | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |     "max": max, | 
					
						
							|  |  |  |     "min": min, | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |     # bitwise: | 
					
						
							|  |  |  |     "xor": operator.xor, | 
					
						
							|  |  |  |     "or": operator.or_, | 
					
						
							|  |  |  |     "and": operator.and_, | 
					
						
							|  |  |  |     "left_shift": operator.lshift, | 
					
						
							|  |  |  |     "right_shift": operator.rshift, | 
					
						
							| 
									
										
										
										
											2023-11-24 17:42:22 +01:00
										 |  |  |     # lists/dicts: | 
					
						
							| 
									
										
										
										
											2023-01-24 21:14:46 -08:00
										 |  |  |     "remove": remove_from_list, | 
					
						
							|  |  |  |     "pop": pop_from_container, | 
					
						
							|  |  |  |     "update": update_dict, | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-12 00:30:43 +01:00
										 |  |  | def get_saving_second(seed_name: str, interval: int = 60) -> int: | 
					
						
							|  |  |  |     # save at expected times so other systems using savegame can expect it | 
					
						
							|  |  |  |     # represents the target second of the auto_save_interval at which to save | 
					
						
							|  |  |  |     return int(hashlib.sha256(seed_name.encode()).hexdigest(), 16) % interval | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  | class Client(Endpoint): | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |     version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  |     tags: typing.List[str] = [] | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |     remote_items: bool | 
					
						
							|  |  |  |     remote_start_inventory: bool | 
					
						
							|  |  |  |     no_items: bool | 
					
						
							|  |  |  |     no_locations: bool | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |     def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context): | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |         super().__init__(socket) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         self.auth = False | 
					
						
							|  |  |  |         self.team = None | 
					
						
							|  |  |  |         self.slot = None | 
					
						
							|  |  |  |         self.send_index = 0 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  |         self.tags = [] | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |         self.messageprocessor = client_message_processor(ctx, self) | 
					
						
							| 
									
										
										
										
											2020-04-19 14:05:58 +02:00
										 |  |  |         self.ctx = weakref.ref(ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def items_handling(self): | 
					
						
							|  |  |  |         if self.no_items: | 
					
						
							|  |  |  |             return 0 | 
					
						
							|  |  |  |         return 1 + (self.remote_items << 1) + (self.remote_start_inventory << 2) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @items_handling.setter | 
					
						
							|  |  |  |     def items_handling(self, value: int): | 
					
						
							|  |  |  |         if not (value & 0b001) and (value & 0b110): | 
					
						
							|  |  |  |             raise ValueError("Invalid flag combination") | 
					
						
							|  |  |  |         self.no_items = not (value & 0b001) | 
					
						
							|  |  |  |         self.remote_items = bool(value & 0b010) | 
					
						
							|  |  |  |         self.remote_start_inventory = bool(value & 0b100) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def name(self) -> str: | 
					
						
							|  |  |  |         ctx = self.ctx() | 
					
						
							|  |  |  |         if ctx: | 
					
						
							|  |  |  |             return ctx.player_names[self.team, self.slot] | 
					
						
							|  |  |  |         return "Deallocated" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  | team_slot = typing.Tuple[int, int] | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | class Context: | 
					
						
							|  |  |  |     dumper = staticmethod(encode) | 
					
						
							|  |  |  |     loader = staticmethod(decode) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-15 15:21:41 +01:00
										 |  |  |     simple_options = {"hint_cost": int, | 
					
						
							|  |  |  |                       "location_check_points": int, | 
					
						
							|  |  |  |                       "server_password": str, | 
					
						
							|  |  |  |                       "password": str, | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                       "release_mode": str, | 
					
						
							| 
									
										
										
										
											2020-11-30 21:07:02 +01:00
										 |  |  |                       "remaining_mode": str, | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |                       "collect_mode": str, | 
					
						
							| 
									
										
										
										
											2020-11-15 15:21:41 +01:00
										 |  |  |                       "item_cheat": bool, | 
					
						
							|  |  |  |                       "compatibility": int} | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     # team -> slot id -> list of clients authenticated to slot. | 
					
						
							|  |  |  |     clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]] | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     locations: LocationStore  # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] | 
					
						
							|  |  |  |     location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]] | 
					
						
							|  |  |  |     hints_used: typing.Dict[typing.Tuple[int, int], int] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |     groups: typing.Dict[int, typing.Set[int]] | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |     save_version = 2 | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |     stored_data: typing.Dict[str, object] | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |     read_data: typing.Dict[str, object] | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |     stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] | 
					
						
							| 
									
										
										
										
											2023-02-13 01:56:20 +01:00
										 |  |  |     slot_info: typing.Dict[int, NetworkSlot] | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |     generator_version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     checksums: typing.Dict[str, str] | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |     item_names: typing.Dict[str, typing.Dict[int, str]] = ( | 
					
						
							|  |  |  |         collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))) | 
					
						
							| 
									
										
										
										
											2022-12-08 21:23:31 +01:00
										 |  |  |     item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |     location_names: typing.Dict[str, typing.Dict[int, str]] = ( | 
					
						
							|  |  |  |         collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))) | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |     location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     all_item_and_group_names: typing.Dict[str, typing.Set[str]] | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |     all_location_and_group_names: typing.Dict[str, typing.Set[str]] | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |     non_hintable_names: typing.Dict[str, typing.AbstractSet[str]] | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |     spheres: typing.List[typing.Dict[int, typing.Set[int]]] | 
					
						
							|  |  |  |     """ each sphere is { player: { location_id, ... } } """ | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |     logger: logging.Logger | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |                  hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |                  remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                  log_network: bool = False, logger: logging.Logger = logging.getLogger()): | 
					
						
							|  |  |  |         self.logger = logger | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |         super(Context, self).__init__() | 
					
						
							| 
									
										
										
										
											2023-02-13 01:56:20 +01:00
										 |  |  |         self.slot_info = {} | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         self.log_network = log_network | 
					
						
							|  |  |  |         self.endpoints = [] | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         self.clients = {} | 
					
						
							| 
									
										
										
										
											2020-07-16 16:57:38 +02:00
										 |  |  |         self.compatibility: int = compatibility | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |         self.shutdown_task = None | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         self.data_filename = None | 
					
						
							|  |  |  |         self.save_filename = None | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |         self.saving = False | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |         self.player_names: typing.Dict[team_slot, str] = {} | 
					
						
							|  |  |  |         self.player_name_lookup: typing.Dict[str, team_slot] = {} | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |         self.connect_names = {}  # names of slots clients can connect to | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |         self.allow_releases = {} | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         self.host = host | 
					
						
							|  |  |  |         self.port = port | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |         self.server_password = server_password | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         self.password = password | 
					
						
							|  |  |  |         self.server = None | 
					
						
							| 
									
										
										
										
											2020-01-10 22:44:07 +01:00
										 |  |  |         self.countdown_timer = 0 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         self.received_items = {} | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |         self.start_inventory = {} | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |         self.name_aliases: typing.Dict[team_slot, str] = {} | 
					
						
							| 
									
										
										
										
											2020-02-09 12:10:12 +01:00
										 |  |  |         self.location_checks = collections.defaultdict(set) | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  |         self.hint_cost = hint_cost | 
					
						
							|  |  |  |         self.location_check_points = location_check_points | 
					
						
							| 
									
										
										
										
											2020-02-22 18:04:35 +01:00
										 |  |  |         self.hints_used = collections.defaultdict(int) | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |         self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |         self.release_mode: str = release_mode | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |         self.remaining_mode: str = remaining_mode | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         self.collect_mode: str = collect_mode | 
					
						
							| 
									
										
										
										
											2020-02-22 19:42:44 +01:00
										 |  |  |         self.item_cheat = item_cheat | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |         self.exit_event = asyncio.Event() | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |         self.client_activity_timers: typing.Dict[ | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |             team_slot, datetime.datetime] = {}  # datetime of last new item check | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |         self.client_connection_timers: typing.Dict[ | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |             team_slot, datetime.datetime] = {}  # datetime of last connection | 
					
						
							|  |  |  |         self.client_game_state: typing.Dict[team_slot, int] = collections.defaultdict(int) | 
					
						
							| 
									
										
										
										
											2020-05-18 05:40:36 +02:00
										 |  |  |         self.er_hint_data: typing.Dict[int, typing.Dict[int, str]] = {} | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |         self.auto_shutdown = auto_shutdown | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         self.commandprocessor = ServerCommandProcessor(self) | 
					
						
							| 
									
										
										
										
											2020-06-13 22:49:57 +02:00
										 |  |  |         self.embedded_blacklist = {"host", "port"} | 
					
						
							| 
									
										
										
										
											2020-06-16 01:05:32 +02:00
										 |  |  |         self.client_ids: typing.Dict[typing.Tuple[int, int], datetime.datetime] = {} | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |         self.auto_save_interval = 60  # in seconds | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |         self.auto_saver_thread: typing.Optional[threading.Thread] = None | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |         self.save_dirty = False | 
					
						
							| 
									
										
										
										
											2020-10-18 23:07:48 +02:00
										 |  |  |         self.tags = ['AP'] | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |         self.games: typing.Dict[int, str] = {} | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |         self.minimum_client_versions: typing.Dict[int, Version] = {} | 
					
						
							| 
									
										
										
										
											2021-04-12 09:36:45 +02:00
										 |  |  |         self.seed_name = "" | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         self.groups = {} | 
					
						
							| 
									
										
										
										
											2022-04-10 14:08:54 -07:00
										 |  |  |         self.group_collected: typing.Dict[int, typing.Set[int]] = {} | 
					
						
							| 
									
										
										
										
											2021-08-01 17:02:38 +02:00
										 |  |  |         self.random = random.Random() | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |         self.stored_data = {} | 
					
						
							|  |  |  |         self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         self.read_data = {} | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |         self.spheres = [] | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         # init empty to satisfy linter, I suppose | 
					
						
							|  |  |  |         self.gamespackage = {} | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         self.checksums = {} | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         self.item_name_groups = {} | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |         self.location_name_groups = {} | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         self.all_item_and_group_names = {} | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |         self.all_location_and_group_names = {} | 
					
						
							| 
									
										
										
										
											2022-09-18 11:49:31 +02:00
										 |  |  |         self.non_hintable_names = collections.defaultdict(frozenset) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self._load_game_data() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     # Data package retrieval | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _load_game_data(self): | 
					
						
							|  |  |  |         import worlds | 
					
						
							|  |  |  |         self.gamespackage = worlds.network_data_package["games"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.item_name_groups = {world_name: world.item_name_groups for world_name, world in | 
					
						
							|  |  |  |                                  worlds.AutoWorldRegister.world_types.items()} | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |         self.location_name_groups = {world_name: world.location_name_groups for world_name, world in | 
					
						
							|  |  |  |                                      worlds.AutoWorldRegister.world_types.items()} | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         for world_name, world in worlds.AutoWorldRegister.world_types.items(): | 
					
						
							|  |  |  |             self.non_hintable_names[world_name] = world.hint_blacklist | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |         for game_package in self.gamespackage.values(): | 
					
						
							|  |  |  |             # remove groups from data sent to clients | 
					
						
							|  |  |  |             del game_package["item_name_groups"] | 
					
						
							|  |  |  |             del game_package["location_name_groups"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _init_game_data(self): | 
					
						
							|  |  |  |         for game_name, game_package in self.gamespackage.items(): | 
					
						
							| 
									
										
										
										
											2023-04-01 19:54:44 +02:00
										 |  |  |             if "checksum" in game_package: | 
					
						
							|  |  |  |                 self.checksums[game_name] = game_package["checksum"] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             for item_name, item_id in game_package["item_name_to_id"].items(): | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |                 self.item_names[game_name][item_id] = item_name | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             for location_name, location_id in game_package["location_name_to_id"].items(): | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |                 self.location_names[game_name][location_id] = location_name | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             self.all_item_and_group_names[game_name] = \ | 
					
						
							|  |  |  |                 set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |             self.all_location_and_group_names[game_name] = \ | 
					
						
							| 
									
										
										
										
											2023-04-01 19:54:44 +02:00
										 |  |  |                 set(game_package["location_name_to_id"]) | set(self.location_name_groups.get(game_name, [])) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |         archipelago_item_names = self.item_names["Archipelago"] | 
					
						
							|  |  |  |         archipelago_location_names = self.location_names["Archipelago"] | 
					
						
							|  |  |  |         for game in [game_name for game_name in self.gamespackage if game_name != "Archipelago"]: | 
					
						
							|  |  |  |             # Add Archipelago items and locations to each data package. | 
					
						
							|  |  |  |             self.item_names[game].update(archipelago_item_names) | 
					
						
							|  |  |  |             self.location_names[game].update(archipelago_location_names) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |     def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: | 
					
						
							|  |  |  |         return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |     def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: | 
					
						
							|  |  |  |         return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     # General networking | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: | 
					
						
							|  |  |  |         if not endpoint.socket or not endpoint.socket.open: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         msg = self.dumper(msgs) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             await endpoint.socket.send(msg) | 
					
						
							|  |  |  |         except websockets.ConnectionClosed: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.exception(f"Exception during send_msgs, could not send {msg}") | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |             await self.disconnect(endpoint) | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             if self.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.info(f"Outgoing message: {msg}") | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool: | 
					
						
							|  |  |  |         if not endpoint.socket or not endpoint.socket.open: | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             await endpoint.socket.send(msg) | 
					
						
							|  |  |  |         except websockets.ConnectionClosed: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.exception("Exception during send_encoded_msgs") | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |             await self.disconnect(endpoint) | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             if self.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.info(f"Outgoing message: {msg}") | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  |     async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool: | 
					
						
							|  |  |  |         sockets = [] | 
					
						
							|  |  |  |         for endpoint in endpoints: | 
					
						
							|  |  |  |             if endpoint.socket and endpoint.socket.open: | 
					
						
							|  |  |  |                 sockets.append(endpoint.socket) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             websockets.broadcast(sockets, msg) | 
					
						
							|  |  |  |         except RuntimeError: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.exception("Exception during broadcast_send_encoded_msgs") | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             if self.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.info(f"Outgoing broadcast: {msg}") | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     def broadcast_all(self, msgs: typing.List[dict]): | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         msgs = self.dumper(msgs) | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  |         endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth) | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |     def broadcast_text_all(self, text: str, additional_arguments: dict = {}): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         self.logger.info("Notice (all): %s" % text) | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |         self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     def broadcast_team(self, team: int, msgs: typing.List[dict]): | 
					
						
							| 
									
										
										
										
											2021-09-09 18:56:43 +02:00
										 |  |  |         msgs = self.dumper(msgs) | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values())) | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]): | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         msgs = self.dumper(msgs) | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.broadcast_send_encoded_msgs(endpoints, msgs)) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     async def disconnect(self, endpoint: Client): | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         if endpoint in self.endpoints: | 
					
						
							|  |  |  |             self.endpoints.remove(endpoint) | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         if endpoint.slot and endpoint in self.clients[endpoint.team][endpoint.slot]: | 
					
						
							|  |  |  |             self.clients[endpoint.team][endpoint.slot].remove(endpoint) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         await on_client_disconnected(self, endpoint) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     def notify_client(self, client: Client, text: str, additional_arguments: dict = {}): | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         if not client.auth: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}])) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}): | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         if not client.auth: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         async_start(self.send_msgs(client, | 
					
						
							|  |  |  |                                    [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments} | 
					
						
							|  |  |  |                                     for text in texts])) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # loading | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |     def load(self, multidatapath: str, use_embedded_server_options: bool = False): | 
					
						
							| 
									
										
										
										
											2021-07-26 09:12:04 +02:00
										 |  |  |         if multidatapath.lower().endswith(".zip"): | 
					
						
							|  |  |  |             import zipfile | 
					
						
							|  |  |  |             with zipfile.ZipFile(multidatapath) as zf: | 
					
						
							|  |  |  |                 for file in zf.namelist(): | 
					
						
							|  |  |  |                     if file.endswith(".archipelago"): | 
					
						
							|  |  |  |                         data = zf.read(file) | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     raise Exception("No .archipelago found in archive.") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             with open(multidatapath, 'rb') as f: | 
					
						
							|  |  |  |                 data = f.read() | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         self._load(self.decompress(data), {}, use_embedded_server_options) | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |         self.data_filename = multidatapath | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-04 03:18:19 +02:00
										 |  |  |     @staticmethod | 
					
						
							| 
									
										
										
										
											2022-01-01 17:18:48 +01:00
										 |  |  |     def decompress(data: bytes) -> dict: | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |         format_version = data[0] | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |         if format_version > 3: | 
					
						
							| 
									
										
										
										
											2022-01-18 08:23:38 +01:00
										 |  |  |             raise Utils.VersionException("Incompatible multidata.") | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |         return restricted_loads(zlib.decompress(data[1:])) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], | 
					
						
							|  |  |  |               use_embedded_server_options: bool): | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         self.read_data = {} | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |         mdata_ver = decoded_obj["minimum_versions"]["server"] | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |         if mdata_ver > version_tuple: | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |             raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |                                f"however this server is of version {version_tuple}") | 
					
						
							|  |  |  |         self.generator_version = Version(*decoded_obj["version"]) | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |         clients_ver = decoded_obj["minimum_versions"].get("clients", {}) | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |         self.minimum_client_versions = {} | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |         for player, version in clients_ver.items(): | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |             self.minimum_client_versions[player] = max(Version(*version), min_client_version) | 
					
						
							| 
									
										
										
										
											2020-12-29 19:23:14 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 01:56:20 +01:00
										 |  |  |         self.slot_info = decoded_obj["slot_info"] | 
					
						
							|  |  |  |         self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} | 
					
						
							|  |  |  |         self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items() | 
					
						
							|  |  |  |                        if slot_info.type == SlotType.group} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.clients = {0: {}} | 
					
						
							|  |  |  |         slot_info: NetworkSlot | 
					
						
							|  |  |  |         slot_id: int | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         team_0 = self.clients[0] | 
					
						
							|  |  |  |         for slot_id, slot_info in self.slot_info.items(): | 
					
						
							|  |  |  |             team_0[slot_id] = [] | 
					
						
							|  |  |  |             self.player_names[0, slot_id] = slot_info.name | 
					
						
							|  |  |  |             self.player_name_lookup[slot_info.name] = 0, slot_id | 
					
						
							|  |  |  |             self.read_data[f"hints_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ | 
					
						
							|  |  |  |                 list(self.get_rechecked_hints(local_team, local_player)) | 
					
						
							| 
									
										
										
										
											2023-11-24 17:14:07 -06:00
										 |  |  |             self.read_data[f"client_status_{0}_{slot_id}"] = lambda local_team=0, local_player=slot_id: \ | 
					
						
							|  |  |  |                 self.client_game_state[local_team, local_player] | 
					
						
							| 
									
										
										
										
											2023-02-13 01:56:20 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-12 09:36:45 +02:00
										 |  |  |         self.seed_name = decoded_obj["seed_name"] | 
					
						
							| 
									
										
										
										
											2021-08-01 17:02:38 +02:00
										 |  |  |         self.random.seed(self.seed_name) | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |         self.connect_names = decoded_obj['connect_names'] | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |         self.locations = LocationStore(decoded_obj.pop("locations"))  # pre-emptively free memory | 
					
						
							| 
									
										
										
										
											2021-04-17 21:03:57 +02:00
										 |  |  |         self.slot_data = decoded_obj['slot_data'] | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         for slot, data in self.slot_data.items(): | 
					
						
							|  |  |  |             self.read_data[f"slot_data_{slot}"] = lambda data=data: data | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |         self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()} | 
					
						
							|  |  |  |                              for player, loc_data in decoded_obj["er_hint_data"].items()} | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |         # load start inventory: | 
					
						
							|  |  |  |         for slot, item_codes in decoded_obj["precollected_items"].items(): | 
					
						
							|  |  |  |             self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes] | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 01:56:20 +01:00
										 |  |  |         for slot, hints in decoded_obj["precollected_hints"].items(): | 
					
						
							|  |  |  |             self.hints[0, slot].update(hints) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |         # declare slots that aren't players as done | 
					
						
							|  |  |  |         for slot, slot_info in self.slot_info.items(): | 
					
						
							|  |  |  |             if slot_info.type.always_goal: | 
					
						
							| 
									
										
										
										
											2021-11-06 08:19:10 +01:00
										 |  |  |                 for team in self.clients: | 
					
						
							|  |  |  |                     self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |         if use_embedded_server_options: | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |             server_options = decoded_obj.get("server_options", {}) | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |             self._set_options(server_options) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         # embedded data package | 
					
						
							| 
									
										
										
										
											2022-12-08 21:23:31 +01:00
										 |  |  |         for game_name, data in decoded_obj.get("datapackage", {}).items(): | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             if game_name in game_data_packages: | 
					
						
							|  |  |  |                 data = game_data_packages[game_name] | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.info(f"Loading embedded data package for game {game_name}") | 
					
						
							| 
									
										
										
										
											2022-12-08 21:23:31 +01:00
										 |  |  |             self.gamespackage[game_name] = data | 
					
						
							|  |  |  |             self.item_name_groups[game_name] = data["item_name_groups"] | 
					
						
							| 
									
										
										
										
											2023-04-01 19:54:44 +02:00
										 |  |  |             if "location_name_groups" in data: | 
					
						
							|  |  |  |                 self.location_name_groups[game_name] = data["location_name_groups"] | 
					
						
							|  |  |  |                 del data["location_name_groups"] | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             del data["item_name_groups"]  # remove from data package, but keep in self.item_name_groups | 
					
						
							| 
									
										
										
										
											2022-12-08 21:23:31 +01:00
										 |  |  |         self._init_game_data() | 
					
						
							|  |  |  |         for game_name, data in self.item_name_groups.items(): | 
					
						
							|  |  |  |             self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame] | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |         for game_name, data in self.location_name_groups.items(): | 
					
						
							|  |  |  |             self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame] | 
					
						
							| 
									
										
										
										
											2022-12-08 21:23:31 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |         # sorted access spheres | 
					
						
							|  |  |  |         self.spheres = decoded_obj.get("spheres", []) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     # saving | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |     def save(self, now=False) -> bool: | 
					
						
							|  |  |  |         if self.saving: | 
					
						
							|  |  |  |             if now: | 
					
						
							|  |  |  |                 self.save_dirty = False | 
					
						
							|  |  |  |                 return self._save() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.save_dirty = True | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |     def _save(self, exit_save: bool = False) -> bool: | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |             encoded_save = pickle.dumps(self.get_save()) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |             with open(self.save_filename, "wb") as f: | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |                 f.write(zlib.compress(encoded_save)) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |         except Exception as e: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.exception(e) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |             return False | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |     def init_save(self, enabled: bool = True): | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |         self.saving = enabled | 
					
						
							|  |  |  |         if self.saving: | 
					
						
							|  |  |  |             if not self.save_filename: | 
					
						
							| 
									
										
										
										
											2021-07-26 09:12:04 +02:00
										 |  |  |                 import os | 
					
						
							|  |  |  |                 name, ext = os.path.splitext(self.data_filename) | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  |                 self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \ | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |                     else self.data_filename + '_' + 'apsave' | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 with open(self.save_filename, 'rb') as f: | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |                     save_data = restricted_loads(zlib.decompress(f.read())) | 
					
						
							|  |  |  |                     self.set_save(save_data) | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |             except FileNotFoundError: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.error('No save data found, starting a new game') | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |             except Exception as e: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.exception(e) | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |             self._start_async_saving() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 18:25:56 +02:00
										 |  |  |     def _start_async_saving(self, atexit_save: bool = True): | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |         if not self.auto_saver_thread: | 
					
						
							|  |  |  |             def save_regularly(): | 
					
						
							| 
									
										
										
										
											2022-12-12 00:30:43 +01:00
										 |  |  |                 # time.time() is platform dependent, so using the expensive datetime method instead | 
					
						
							|  |  |  |                 def get_datetime_second(): | 
					
						
							|  |  |  |                     now = datetime.datetime.now() | 
					
						
							|  |  |  |                     return now.second + now.microsecond * 0.000001 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 second = get_saving_second(self.seed_name, self.auto_save_interval) | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |                 while not self.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |                     try: | 
					
						
							| 
									
										
										
										
											2022-12-12 00:30:43 +01:00
										 |  |  |                         next_wakeup = (second - get_datetime_second()) % self.auto_save_interval | 
					
						
							|  |  |  |                         time.sleep(max(1.0, next_wakeup)) | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |                         if self.save_dirty: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                             self.logger.debug("Saving via thread.") | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |                             self._save() | 
					
						
							|  |  |  |                     except OperationalError as e: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                         self.logger.exception(e) | 
					
						
							|  |  |  |                         self.logger.info(f"Saving failed. Retry in {self.auto_save_interval} seconds.") | 
					
						
							| 
									
										
										
										
											2022-06-08 00:35:35 +02:00
										 |  |  |                     else: | 
					
						
							| 
									
										
										
										
											2020-06-20 20:03:06 +02:00
										 |  |  |                         self.save_dirty = False | 
					
						
							|  |  |  |             self.auto_saver_thread = threading.Thread(target=save_regularly, daemon=True) | 
					
						
							|  |  |  |             self.auto_saver_thread.start() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-19 18:25:56 +02:00
										 |  |  |             if atexit_save: | 
					
						
							|  |  |  |                 import atexit | 
					
						
							|  |  |  |                 atexit.register(self._save, True)  # make sure we save on exit too | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  |     def get_save(self) -> dict: | 
					
						
							| 
									
										
										
										
											2021-06-25 21:04:37 +02:00
										 |  |  |         self.recheck_hints() | 
					
						
							| 
									
										
										
										
											2020-04-22 15:50:14 +02:00
										 |  |  |         d = { | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |             "version": self.save_version, | 
					
						
							| 
									
										
										
										
											2021-05-13 01:58:53 +02:00
										 |  |  |             "connect_names": self.connect_names, | 
					
						
							|  |  |  |             "received_items": self.received_items, | 
					
						
							|  |  |  |             "hints_used": dict(self.hints_used), | 
					
						
							|  |  |  |             "hints": dict(self.hints), | 
					
						
							|  |  |  |             "location_checks": dict(self.location_checks), | 
					
						
							|  |  |  |             "name_aliases": self.name_aliases, | 
					
						
							|  |  |  |             "client_game_state": dict(self.client_game_state), | 
					
						
							| 
									
										
										
										
											2020-06-23 14:01:01 +02:00
										 |  |  |             "client_activity_timers": tuple( | 
					
						
							|  |  |  |                 (key, value.timestamp()) for key, value in self.client_activity_timers.items()), | 
					
						
							|  |  |  |             "client_connection_timers": tuple( | 
					
						
							|  |  |  |                 (key, value.timestamp()) for key, value in self.client_connection_timers.items()), | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |             "random_state": self.random.getstate(), | 
					
						
							| 
									
										
										
										
											2022-04-10 14:08:54 -07:00
										 |  |  |             "group_collected": dict(self.group_collected), | 
					
						
							| 
									
										
										
										
											2022-03-01 18:39:58 -05:00
										 |  |  |             "stored_data": self.stored_data, | 
					
						
							|  |  |  |             "game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points, | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                              "server_password": self.server_password, "password": self.password, | 
					
						
							| 
									
										
										
										
											2023-04-02 22:49:40 +02:00
										 |  |  |                              "release_mode": self.release_mode, | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                              "remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode, | 
					
						
							|  |  |  |                              "item_cheat": self.item_cheat, "compatibility": self.compatibility} | 
					
						
							| 
									
										
										
										
											2022-03-01 18:39:58 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-05-13 01:58:53 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-22 15:50:14 +02:00
										 |  |  |         return d | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  |     def set_save(self, savedata: dict): | 
					
						
							| 
									
										
										
										
											2021-05-13 01:58:53 +02:00
										 |  |  |         if self.connect_names != savedata["connect_names"]: | 
					
						
							|  |  |  |             raise Exception("This savegame does not appear to match the loaded multiworld.") | 
					
						
							| 
									
										
										
										
											2022-05-31 04:20:26 +02:00
										 |  |  |         if savedata["version"] > self.save_version: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |             raise Exception("This savegame is newer than the server.") | 
					
						
							| 
									
										
										
										
											2022-05-31 04:20:26 +02:00
										 |  |  |         self.received_items = savedata["received_items"] | 
					
						
							| 
									
										
										
										
											2021-05-13 01:58:53 +02:00
										 |  |  |         self.hints_used.update(savedata["hints_used"]) | 
					
						
							|  |  |  |         self.hints.update(savedata["hints"]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.name_aliases.update(savedata["name_aliases"]) | 
					
						
							|  |  |  |         self.client_game_state.update(savedata["client_game_state"]) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         self.client_connection_timers.update( | 
					
						
							|  |  |  |             {tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value | 
					
						
							|  |  |  |              in savedata["client_connection_timers"]}) | 
					
						
							|  |  |  |         self.client_activity_timers.update( | 
					
						
							|  |  |  |             {tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value | 
					
						
							|  |  |  |              in savedata["client_activity_timers"]}) | 
					
						
							| 
									
										
										
										
											2021-05-13 01:58:53 +02:00
										 |  |  |         self.location_checks.update(savedata["location_checks"]) | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |         self.random.setstate(savedata["random_state"]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-01 18:39:58 -05:00
										 |  |  |         if "game_options" in savedata: | 
					
						
							|  |  |  |             self.hint_cost = savedata["game_options"]["hint_cost"] | 
					
						
							|  |  |  |             self.location_check_points = savedata["game_options"]["location_check_points"] | 
					
						
							|  |  |  |             self.server_password = savedata["game_options"]["server_password"] | 
					
						
							|  |  |  |             self.password = savedata["game_options"]["password"] | 
					
						
							| 
									
										
										
										
											2024-04-12 15:25:33 -04:00
										 |  |  |             self.release_mode = savedata["game_options"]["release_mode"] | 
					
						
							| 
									
										
										
										
											2022-03-01 18:39:58 -05:00
										 |  |  |             self.remaining_mode = savedata["game_options"]["remaining_mode"] | 
					
						
							|  |  |  |             self.collect_mode = savedata["game_options"]["collect_mode"] | 
					
						
							|  |  |  |             self.item_cheat = savedata["game_options"]["item_cheat"] | 
					
						
							|  |  |  |             self.compatibility = savedata["game_options"]["compatibility"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 14:08:54 -07:00
										 |  |  |         if "group_collected" in savedata: | 
					
						
							|  |  |  |             self.group_collected = savedata["group_collected"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |         if "stored_data" in savedata: | 
					
						
							|  |  |  |             self.stored_data = savedata["stored_data"] | 
					
						
							| 
									
										
										
										
											2022-12-11 02:59:17 +01:00
										 |  |  |         # count items and slots from lists for items_handling = remote | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         self.logger.info( | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |             f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items ' | 
					
						
							|  |  |  |             f'for {sum(k[2] for k in self.received_items)} players') | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     # rest | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     def get_hint_cost(self, slot): | 
					
						
							|  |  |  |         if self.hint_cost: | 
					
						
							| 
									
										
										
										
											2023-03-24 23:14:34 +01:00
										 |  |  |             return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot]))) | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         return 0 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |     def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None): | 
					
						
							|  |  |  |         for hint_team, hint_slot in self.hints: | 
					
						
							|  |  |  |             if (team is None or team == hint_team) and (slot is None or slot == hint_slot): | 
					
						
							|  |  |  |                 self.hints[hint_team, hint_slot] = { | 
					
						
							|  |  |  |                     hint.re_check(self, hint_team) for hint in | 
					
						
							|  |  |  |                     self.hints[hint_team, hint_slot] | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_rechecked_hints(self, team: int, slot: int): | 
					
						
							|  |  |  |         self.recheck_hints(team, slot) | 
					
						
							|  |  |  |         return self.hints[team, slot] | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |     def get_sphere(self, player: int, location_id: int) -> int: | 
					
						
							|  |  |  |         """Get sphere of a location, -1 if spheres are not available.""" | 
					
						
							|  |  |  |         if self.spheres: | 
					
						
							|  |  |  |             for i, sphere in enumerate(self.spheres): | 
					
						
							|  |  |  |                 if location_id in sphere.get(player, set()): | 
					
						
							|  |  |  |                     return i | 
					
						
							|  |  |  |             raise KeyError(f"No Sphere found for location ID {location_id} belonging to player {player}. " | 
					
						
							|  |  |  |                            f"Location or player may not exist.") | 
					
						
							|  |  |  |         return -1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     def get_players_package(self): | 
					
						
							|  |  |  |         return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()] | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 04:40:35 +02:00
										 |  |  |     def slot_set(self, slot) -> typing.Set[int]: | 
					
						
							|  |  |  |         """Returns the slot IDs that concern that slot,
 | 
					
						
							|  |  |  |         as in expands groups out and returns back the input for solo."""
 | 
					
						
							|  |  |  |         return self.groups.get(slot, {slot}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     def _set_options(self, server_options: dict): | 
					
						
							|  |  |  |         for key, value in server_options.items(): | 
					
						
							|  |  |  |             data_type = self.simple_options.get(key, None) | 
					
						
							|  |  |  |             if data_type is not None: | 
					
						
							|  |  |  |                 if value not in {False, True, None}:  # some can be boolean OR text, such as password | 
					
						
							|  |  |  |                     try: | 
					
						
							|  |  |  |                         value = data_type(value) | 
					
						
							|  |  |  |                     except Exception as e: | 
					
						
							|  |  |  |                         try: | 
					
						
							|  |  |  |                             raise Exception(f"Could not set server option {key}, skipping.") from e | 
					
						
							|  |  |  |                         except Exception as e: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                             self.logger.exception(e) | 
					
						
							|  |  |  |                 self.logger.debug(f"Setting server option {key} to {value} from supplied multidata") | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |                 setattr(self, key, value) | 
					
						
							|  |  |  |             elif key == "disable_item_cheat": | 
					
						
							|  |  |  |                 self.item_cheat = not bool(value) | 
					
						
							|  |  |  |             else: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 self.logger.debug(f"Unrecognized server option {key}") | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |     def get_aliased_name(self, team: int, slot: int): | 
					
						
							|  |  |  |         if (team, slot) in self.name_aliases: | 
					
						
							| 
									
										
										
										
											2021-11-04 08:57:27 +01:00
										 |  |  |             return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})" | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-11-04 08:57:27 +01:00
										 |  |  |             return self.player_names[team, slot] | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 06:34:48 +01:00
										 |  |  |     def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False, | 
					
						
							|  |  |  |                      recipients: typing.Sequence[int] = None): | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         """Send and remember hints.""" | 
					
						
							|  |  |  |         if only_new: | 
					
						
							|  |  |  |             hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]] | 
					
						
							|  |  |  |         if not hints: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         new_hint_events: typing.Set[int] = set() | 
					
						
							|  |  |  |         concerns = collections.defaultdict(list) | 
					
						
							|  |  |  |         for hint in sorted(hints, key=operator.attrgetter('found'), reverse=True): | 
					
						
							|  |  |  |             data = (hint, hint.as_network_message()) | 
					
						
							|  |  |  |             for player in self.slot_set(hint.receiving_player): | 
					
						
							|  |  |  |                 concerns[player].append(data) | 
					
						
							|  |  |  |             if not hint.local and data not in concerns[hint.finding_player]: | 
					
						
							|  |  |  |                 concerns[hint.finding_player].append(data) | 
					
						
							|  |  |  |             # remember hints in all cases | 
					
						
							|  |  |  |             if not hint.found: | 
					
						
							|  |  |  |                 # since hints are bidirectional, finding player and receiving player, | 
					
						
							|  |  |  |                 # we can check once if hint already exists | 
					
						
							|  |  |  |                 if hint not in self.hints[team, hint.finding_player]: | 
					
						
							|  |  |  |                     self.hints[team, hint.finding_player].add(hint) | 
					
						
							|  |  |  |                     new_hint_events.add(hint.finding_player) | 
					
						
							|  |  |  |                     for player in self.slot_set(hint.receiving_player): | 
					
						
							|  |  |  |                         self.hints[team, player].add(hint) | 
					
						
							|  |  |  |                         new_hint_events.add(player) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint))) | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         for slot in new_hint_events: | 
					
						
							|  |  |  |             self.on_new_hint(team, slot) | 
					
						
							|  |  |  |         for slot, hint_data in concerns.items(): | 
					
						
							| 
									
										
										
										
											2024-03-03 06:34:48 +01:00
										 |  |  |             if recipients is None or slot in recipients: | 
					
						
							|  |  |  |                 clients = self.clients[team].get(slot) | 
					
						
							|  |  |  |                 if not clients: | 
					
						
							|  |  |  |                     continue | 
					
						
							| 
									
										
										
										
											2024-05-03 22:28:09 -04:00
										 |  |  |                 client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)] | 
					
						
							| 
									
										
										
										
											2024-03-03 06:34:48 +01:00
										 |  |  |                 for client in clients: | 
					
						
							|  |  |  |                     async_start(self.send_msgs(client, client_hints)) | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # "events" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-11 16:09:08 +01:00
										 |  |  |     def on_goal_achieved(self, client: Client): | 
					
						
							|  |  |  |         finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ | 
					
						
							|  |  |  |                        f' has completed their goal.' | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         self.broadcast_text_all(finished_msg, {"type": "Goal", "team": client.team, "slot": client.slot}) | 
					
						
							| 
									
										
										
										
											2022-08-09 06:21:05 +02:00
										 |  |  |         if "auto" in self.collect_mode: | 
					
						
							|  |  |  |             collect_player(self, client.team, client.slot) | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |         if "auto" in self.release_mode: | 
					
						
							|  |  |  |             release_player(self, client.team, client.slot) | 
					
						
							| 
									
										
										
										
											2022-09-05 10:07:10 +02:00
										 |  |  |         self.save()  # save goal completion flag | 
					
						
							| 
									
										
										
										
											2021-11-11 16:09:08 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |     def on_new_hint(self, team: int, slot: int): | 
					
						
							| 
									
										
										
										
											2024-03-12 19:58:02 +01:00
										 |  |  |         self.on_changed_hints(team, slot) | 
					
						
							| 
									
										
										
										
											2023-04-10 14:44:20 -05:00
										 |  |  |         self.broadcast(self.clients[team][slot], [{ | 
					
						
							|  |  |  |             "cmd": "RoomUpdate", | 
					
						
							|  |  |  |             "hint_points": get_slot_points(self, team, slot) | 
					
						
							|  |  |  |         }]) | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 19:58:02 +01:00
										 |  |  |     def on_changed_hints(self, team: int, slot: int): | 
					
						
							|  |  |  |         key: str = f"_read_hints_{team}_{slot}" | 
					
						
							|  |  |  |         targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) | 
					
						
							|  |  |  |         if targets: | 
					
						
							|  |  |  |             self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-24 17:14:07 -06:00
										 |  |  |     def on_client_status_change(self, team: int, slot: int): | 
					
						
							|  |  |  |         key: str = f"_read_client_status_{team}_{slot}" | 
					
						
							|  |  |  |         targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) | 
					
						
							|  |  |  |         if targets: | 
					
						
							|  |  |  |             self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.client_game_state[team, slot]}]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | def update_aliases(ctx: Context, team: int): | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |     cmd = ctx.dumper([{"cmd": "RoomUpdate", | 
					
						
							|  |  |  |                        "players": ctx.get_players_package()}]) | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for clients in ctx.clients[team].values(): | 
					
						
							|  |  |  |         for client in clients: | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |             async_start(ctx.send_encoded_msgs(client, cmd)) | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-15 20:55:21 +01:00
										 |  |  | async def server(websocket, path: str = "/", ctx: Context = None): | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |     client = Client(websocket, ctx) | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |     ctx.endpoints.append(client) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-04-07 02:37:21 +02:00
										 |  |  |         if ctx.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.info("Incoming connection") | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         await on_client_connected(ctx, client) | 
					
						
							| 
									
										
										
										
											2021-04-07 02:37:21 +02:00
										 |  |  |         if ctx.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.info("Sent Room Info") | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         async for data in websocket: | 
					
						
							| 
									
										
										
										
											2021-04-07 02:37:21 +02:00
										 |  |  |             if ctx.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |                 ctx.logger.info(f"Incoming message: {data}") | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |             for msg in decode(data): | 
					
						
							|  |  |  |                 await process_client_cmd(ctx, client, msg) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     except Exception as e: | 
					
						
							| 
									
										
										
										
											2019-12-16 18:39:00 +01:00
										 |  |  |         if not isinstance(e, websockets.WebSocketException): | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.exception(e) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     finally: | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |         if ctx.log_network: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.info("Disconnected") | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |         await ctx.disconnect(client) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | async def on_client_connected(ctx: Context, client: Client): | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     players = [] | 
					
						
							|  |  |  |     for team, clients in ctx.clients.items(): | 
					
						
							|  |  |  |         for slot, connected_clients in clients.items(): | 
					
						
							|  |  |  |             if connected_clients: | 
					
						
							|  |  |  |                 name = ctx.player_names[team, slot] | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |                 players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name)) | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)} | 
					
						
							| 
									
										
										
										
											2023-03-21 22:02:21 +01:00
										 |  |  |     games.add("Archipelago") | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |     await ctx.send_msgs(client, [{ | 
					
						
							|  |  |  |         'cmd': 'RoomInfo', | 
					
						
							| 
									
										
										
										
											2021-10-15 22:08:24 +02:00
										 |  |  |         'password': bool(ctx.password), | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         'games': games, | 
					
						
							| 
									
										
										
										
											2020-02-16 15:35:01 +01:00
										 |  |  |         # tags are for additional features in the communication. | 
					
						
							|  |  |  |         # Name them by feature or fork, as you feel is appropriate. | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |         'tags': ctx.tags, | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |         'version': version_tuple, | 
					
						
							|  |  |  |         'generator_version': ctx.generator_version, | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  |         'permissions': get_permissions(ctx), | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  |         'hint_cost': ctx.hint_cost, | 
					
						
							| 
									
										
										
										
											2021-02-25 02:07:28 +01:00
										 |  |  |         'location_check_points': ctx.location_check_points, | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         'datapackage_checksums': {game: game_data["checksum"] for game, game_data | 
					
						
							| 
									
										
										
										
											2023-04-01 22:40:14 +02:00
										 |  |  |                                   in ctx.gamespackage.items() if game in games and "checksum" in game_data}, | 
					
						
							| 
									
										
										
										
											2021-11-07 11:37:50 +01:00
										 |  |  |         'seed_name': ctx.seed_name, | 
					
						
							|  |  |  |         'time': time.time(), | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |     }]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  | def get_permissions(ctx) -> typing.Dict[str, Permission]: | 
					
						
							|  |  |  |     return { | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |         "release": Permission.from_text(ctx.release_mode), | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  |         "remaining": Permission.from_text(ctx.remaining_mode), | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         "collect": Permission.from_text(ctx.collect_mode) | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | async def on_client_disconnected(ctx: Context, client: Client): | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     if client.auth: | 
					
						
							|  |  |  |         await on_client_left(ctx, client) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  | _non_game_messages = {"HintGame": "hinting", "Tracker": "tracking", "TextOnly": "viewing"} | 
					
						
							|  |  |  | """ { tag: ui_message } """ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | async def on_client_joined(ctx: Context, client: Client): | 
					
						
							| 
									
										
										
										
											2023-03-21 09:50:50 -05:00
										 |  |  |     if ctx.client_game_state[client.team, client.slot] == ClientStatus.CLIENT_UNKNOWN: | 
					
						
							|  |  |  |         update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) | 
					
						
							| 
									
										
										
										
											2020-07-13 03:38:19 +02:00
										 |  |  |     version_str = '.'.join(str(x) for x in client.version) | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for tag, verb in _non_game_messages.items(): | 
					
						
							|  |  |  |         if tag in client.tags: | 
					
						
							|  |  |  |             final_verb = verb | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         final_verb = "playing" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |     ctx.broadcast_text_all( | 
					
						
							| 
									
										
										
										
											2021-07-29 16:21:11 +02:00
										 |  |  |         f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) " | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  |         f"{final_verb} {ctx.games[client.slot]} has joined. " | 
					
						
							| 
									
										
										
										
											2023-07-27 20:31:48 -05:00
										 |  |  |         f"Client({version_str}), {client.tags}.", | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         {"type": "Join", "team": client.team, "slot": client.slot, "tags": client.tags}) | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |     ctx.notify_client(client, "Now that you are connected, " | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |                               "you can use !help to list commands to run via the server. " | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |                               "If your client supports it, " | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |                               "you may have additional local commands you can list with /help.", | 
					
						
							|  |  |  |                       {"type": "Tutorial"}) | 
					
						
							| 
									
										
										
										
											2020-06-23 14:01:01 +02:00
										 |  |  |     ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | async def on_client_left(ctx: Context, client: Client): | 
					
						
							| 
									
										
										
										
											2023-03-21 09:50:50 -05:00
										 |  |  |     if len(ctx.clients[client.team][client.slot]) < 1: | 
					
						
							|  |  |  |         update_client_status(ctx, client, ClientStatus.CLIENT_UNKNOWN) | 
					
						
							|  |  |  |         ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     version_str = '.'.join(str(x) for x in client.version) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for tag, verb in _non_game_messages.items(): | 
					
						
							|  |  |  |         if tag in client.tags: | 
					
						
							|  |  |  |             final_verb = f"stopped {verb}" | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         final_verb = "left" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |     ctx.broadcast_text_all( | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  |         f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has {final_verb} the game. " | 
					
						
							|  |  |  |         f"Client({version_str}), {client.tags}.", | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         {"type": "Part", "team": client.team, "slot": client.slot}) | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-23 01:02:10 +02:00
										 |  |  | async def countdown(ctx: Context, timer: int): | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |     ctx.broadcast_text_all(f"[Server]: Starting countdown of {timer}s", {"type": "Countdown", "countdown": timer}) | 
					
						
							| 
									
										
										
										
											2020-01-10 22:44:07 +01:00
										 |  |  |     if ctx.countdown_timer: | 
					
						
							| 
									
										
										
										
											2020-03-11 23:08:16 +01:00
										 |  |  |         ctx.countdown_timer = timer  # timer is already running, set it to a different time | 
					
						
							|  |  |  |     else: | 
					
						
							| 
									
										
										
										
											2020-01-10 22:44:07 +01:00
										 |  |  |         ctx.countdown_timer = timer | 
					
						
							| 
									
										
										
										
											2020-03-11 23:08:16 +01:00
										 |  |  |         while ctx.countdown_timer > 0: | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |             ctx.broadcast_text_all(f"[Server]: {ctx.countdown_timer}", | 
					
						
							|  |  |  |                 {"type": "Countdown", "countdown": ctx.countdown_timer}) | 
					
						
							| 
									
										
										
										
											2020-03-11 23:08:16 +01:00
										 |  |  |             ctx.countdown_timer -= 1 | 
					
						
							|  |  |  |             await asyncio.sleep(1) | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |         ctx.broadcast_text_all(f"[Server]: GO", {"type": "Countdown", "countdown": 0}) | 
					
						
							| 
									
										
										
										
											2020-07-05 21:46:44 +02:00
										 |  |  |         ctx.countdown_timer = 0 | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-09 21:28:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-19 14:51:48 +02:00
										 |  |  | def get_players_string(ctx: Context): | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |     auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-11 09:15:39 +01:00
										 |  |  |     player_names = sorted(ctx.player_names.keys()) | 
					
						
							| 
									
										
										
										
											2020-03-05 02:31:26 +01:00
										 |  |  |     current_team = -1 | 
					
						
							|  |  |  |     text = '' | 
					
						
							| 
									
										
										
										
											2022-03-14 20:31:57 +01:00
										 |  |  |     total = 0 | 
					
						
							| 
									
										
										
										
											2020-03-11 09:15:39 +01:00
										 |  |  |     for team, slot in player_names: | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         if ctx.slot_info[slot].type == SlotType.player: | 
					
						
							| 
									
										
										
										
											2022-03-14 20:31:57 +01:00
										 |  |  |             total += 1 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             player_name = ctx.player_names[team, slot] | 
					
						
							|  |  |  |             if team != current_team: | 
					
						
							|  |  |  |                 text += f':: Team #{team + 1}: ' | 
					
						
							|  |  |  |                 current_team = team | 
					
						
							|  |  |  |             if (team, slot) in auth_clients: | 
					
						
							|  |  |  |                 text += f'{player_name} ' | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 text += f'({player_name}) ' | 
					
						
							| 
									
										
										
										
											2022-03-14 20:31:57 +01:00
										 |  |  |     return f'{len(auth_clients)} players of {total} connected ' + text[:-1] | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  | def get_status_string(ctx: Context, team: int, tag: str): | 
					
						
							|  |  |  |     text = f"Player Status on team {team}:" | 
					
						
							| 
									
										
										
										
											2021-11-04 09:01:14 +01:00
										 |  |  |     for slot in ctx.locations: | 
					
						
							|  |  |  |         connected = len(ctx.clients[team][slot]) | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  |         tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags]) | 
					
						
							| 
									
										
										
										
											2021-11-04 09:01:14 +01:00
										 |  |  |         completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})" | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  |         tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else "" | 
					
						
							| 
									
										
										
										
											2021-11-04 09:01:14 +01:00
										 |  |  |         goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "." | 
					
						
							|  |  |  |         text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \ | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  |                 f"{tag_text}{goal_text} {completion_text}" | 
					
						
							| 
									
										
										
										
											2021-11-04 08:57:27 +01:00
										 |  |  |     return text | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  | def get_received_items(ctx: Context, team: int, player: int, remote_items: bool) -> typing.List[NetworkItem]: | 
					
						
							|  |  |  |     return ctx.received_items.setdefault((team, player, remote_items), []) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | def get_start_inventory(ctx: Context, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |     return ctx.start_inventory.setdefault(player, []) if remote_start_inventory else [] | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | def send_new_items(ctx: Context): | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     for team, clients in ctx.clients.items(): | 
					
						
							|  |  |  |         for slot, clients in clients.items(): | 
					
						
							|  |  |  |             for client in clients: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                 if client.no_items: | 
					
						
							|  |  |  |                     continue | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                 items = get_received_items(ctx, team, slot, client.remote_items) | 
					
						
							|  |  |  |                 if len(start_inventory) + len(items) > client.send_index: | 
					
						
							|  |  |  |                     first_new_item = max(0, client.send_index - len(start_inventory)) | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |                     async_start(ctx.send_msgs(client, [{ | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |                         "cmd": "ReceivedItems", | 
					
						
							|  |  |  |                         "index": client.send_index, | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                         "items": start_inventory[client.send_index:] + items[first_new_item:]}])) | 
					
						
							|  |  |  |                     client.send_index = len(start_inventory) + len(items) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  | def update_checked_locations(ctx: Context, team: int, slot: int): | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |     ctx.broadcast(ctx.clients[team][slot], | 
					
						
							|  |  |  |                   [{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}]) | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  | def release_player(ctx: Context, team: int, slot: int): | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     """register any locations that are in the multidata""" | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |     all_locations = set(ctx.locations[slot]) | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     ctx.broadcast_text_all("%s (Team #%d) has released all remaining items from their world." | 
					
						
							|  |  |  |                            % (ctx.player_names[(team, slot)], team + 1), | 
					
						
							|  |  |  |                            {"type": "Release", "team": team, "slot": slot}) | 
					
						
							| 
									
										
										
										
											2020-01-14 10:42:27 +01:00
										 |  |  |     register_location_checks(ctx, team, slot, all_locations) | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     update_checked_locations(ctx, team, slot) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 14:08:54 -07:00
										 |  |  | def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False): | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     """register any locations that are in the multidata, pointing towards this player""" | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     all_locations = ctx.locations.get_for_player(slot) | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     ctx.broadcast_text_all("%s (Team #%d) has collected their items from other worlds." | 
					
						
							|  |  |  |                            % (ctx.player_names[(team, slot)], team + 1), | 
					
						
							|  |  |  |                            {"type": "Collect", "team": team, "slot": slot}) | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     for source_player, location_ids in all_locations.items(): | 
					
						
							| 
									
										
										
										
											2022-01-09 23:15:41 +01:00
										 |  |  |         register_location_checks(ctx, team, source_player, location_ids, count_activity=False) | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         update_checked_locations(ctx, team, source_player) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-10 14:08:54 -07:00
										 |  |  |     if not is_group: | 
					
						
							|  |  |  |         for group, group_players in ctx.groups.items(): | 
					
						
							|  |  |  |             if slot in group_players: | 
					
						
							|  |  |  |                 group_collected_players = ctx.group_collected.setdefault(group, set()) | 
					
						
							|  |  |  |                 group_collected_players.add(slot) | 
					
						
							|  |  |  |                 if set(group_players) == group_collected_players: | 
					
						
							|  |  |  |                     collect_player(ctx, team, group, True) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  | def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     return ctx.locations.get_remaining(ctx.location_checks, team, slot) | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 17:35:12 +01:00
										 |  |  | def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem): | 
					
						
							| 
									
										
										
										
											2022-06-08 04:40:35 +02:00
										 |  |  |     for target in ctx.slot_set(target_slot): | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         for item in items: | 
					
						
							| 
									
										
										
										
											2022-02-05 17:35:12 +01:00
										 |  |  |             if item.player != target_slot: | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 get_received_items(ctx, team, target, False).append(item) | 
					
						
							|  |  |  |             get_received_items(ctx, team, target, True).append(item) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-09 23:15:41 +01:00
										 |  |  | def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int], | 
					
						
							|  |  |  |                              count_activity: bool = True): | 
					
						
							| 
									
										
										
										
											2020-06-03 21:07:32 +02:00
										 |  |  |     new_locations = set(locations) - ctx.location_checks[team, slot] | 
					
						
							| 
									
										
										
										
											2021-11-23 20:16:48 +01:00
										 |  |  |     new_locations.intersection_update(ctx.locations[slot])  # ignore location IDs unknown to this multidata | 
					
						
							| 
									
										
										
										
											2020-06-03 21:07:32 +02:00
										 |  |  |     if new_locations: | 
					
						
							| 
									
										
										
										
											2022-01-09 23:15:41 +01:00
										 |  |  |         if count_activity: | 
					
						
							|  |  |  |             ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc) | 
					
						
							| 
									
										
										
										
											2020-06-03 21:07:32 +02:00
										 |  |  |         for location in new_locations: | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |             item_id, target_player, flags = ctx.locations[slot][location] | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |             new_item = NetworkItem(item_id, location, slot, flags) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             send_items_to(ctx, team, target_player, new_item) | 
					
						
							| 
									
										
										
										
											2021-11-23 20:16:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % ( | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |                 team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id], | 
					
						
							|  |  |  |                 ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location])) | 
					
						
							| 
									
										
										
										
											2021-11-23 20:16:48 +01:00
										 |  |  |             info_text = json_format_send_event(new_item, target_player) | 
					
						
							|  |  |  |             ctx.broadcast_team(team, [info_text]) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         ctx.location_checks[team, slot] |= new_locations | 
					
						
							| 
									
										
										
										
											2020-06-03 21:07:32 +02:00
										 |  |  |         send_new_items(ctx) | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         ctx.broadcast(ctx.clients[team][slot], [{ | 
					
						
							|  |  |  |             "cmd": "RoomUpdate", | 
					
						
							|  |  |  |             "hint_points": get_slot_points(ctx, team, slot), | 
					
						
							| 
									
										
										
										
											2021-11-23 20:16:48 +01:00
										 |  |  |             "checked_locations": new_locations,  # send back new checks only | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         }]) | 
					
						
							| 
									
										
										
										
											2024-03-12 19:58:02 +01:00
										 |  |  |         old_hints = ctx.hints[team, slot].copy() | 
					
						
							|  |  |  |         ctx.recheck_hints(team, slot) | 
					
						
							|  |  |  |         if old_hints != ctx.hints[team, slot]: | 
					
						
							|  |  |  |             ctx.on_changed_hints(team, slot) | 
					
						
							| 
									
										
										
										
											2020-06-21 17:04:25 +02:00
										 |  |  |         ctx.save() | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  | def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: | 
					
						
							| 
									
										
										
										
											2020-02-11 00:44:28 +01:00
										 |  |  |     hints = [] | 
					
						
							| 
									
										
										
										
											2022-06-08 04:40:35 +02:00
										 |  |  |     slots: typing.Set[int] = {slot} | 
					
						
							| 
									
										
										
										
											2022-02-18 13:03:55 -08:00
										 |  |  |     for group_id, group in ctx.groups.items(): | 
					
						
							|  |  |  |         if slot in group: | 
					
						
							| 
									
										
										
										
											2022-06-08 04:40:35 +02:00
										 |  |  |             slots.add(group_id) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |     seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     for finding_player, location_id, item_id, receiving_player, item_flags \ | 
					
						
							|  |  |  |             in ctx.locations.find_item(slots, seeked_item_id): | 
					
						
							|  |  |  |         found = location_id in ctx.location_checks[team, finding_player] | 
					
						
							|  |  |  |         entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "") | 
					
						
							|  |  |  |         hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance, | 
					
						
							|  |  |  |                                    item_flags)) | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-11 00:44:28 +01:00
										 |  |  |     return hints | 
					
						
							| 
									
										
										
										
											2020-02-09 05:28:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  | def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  |     return collect_hint_location_id(ctx, team, slot, seeked_location) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]: | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |     result = ctx.locations[slot].get(seeked_location, (None, None, None)) | 
					
						
							| 
									
										
										
										
											2022-03-31 04:54:35 +02:00
										 |  |  |     if any(result): | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |         item_id, receiving_player, item_flags = result | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |         found = seeked_location in ctx.location_checks[team, slot] | 
					
						
							|  |  |  |         entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "") | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)] | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |     return [] | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: | 
					
						
							| 
									
										
										
										
											2020-05-18 05:40:36 +02:00
										 |  |  |     text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |            f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \ | 
					
						
							|  |  |  |            f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \ | 
					
						
							| 
									
										
										
										
											2020-05-18 05:40:36 +02:00
										 |  |  |            f"in {ctx.player_names[team, hint.finding_player]}'s World" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if hint.entrance: | 
					
						
							|  |  |  |         text += f" at {hint.entrance}" | 
					
						
							|  |  |  |     return text + (". (found)" if hint.found else ".") | 
					
						
							| 
									
										
										
										
											2020-02-16 15:32:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | def json_format_send_event(net_item: NetworkItem, receiving_player: int): | 
					
						
							|  |  |  |     parts = [] | 
					
						
							|  |  |  |     NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id) | 
					
						
							| 
									
										
										
										
											2021-04-03 20:02:15 +02:00
										 |  |  |     if net_item.player == receiving_player: | 
					
						
							|  |  |  |         NetUtils.add_json_text(parts, " found their ") | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         NetUtils.add_json_item(parts, net_item.item, net_item.player, net_item.flags) | 
					
						
							| 
									
										
										
										
											2021-04-03 20:02:15 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |         NetUtils.add_json_text(parts, " sent ") | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         NetUtils.add_json_item(parts, net_item.item, receiving_player, net_item.flags) | 
					
						
							| 
									
										
										
										
											2021-04-03 20:02:15 +02:00
										 |  |  |         NetUtils.add_json_text(parts, " to ") | 
					
						
							|  |  |  |         NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |     NetUtils.add_json_text(parts, " (") | 
					
						
							| 
									
										
										
										
											2021-11-07 14:42:05 +01:00
										 |  |  |     NetUtils.add_json_location(parts, net_item.location, net_item.player) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |     NetUtils.add_json_text(parts, ")") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:36:50 +01:00
										 |  |  |     return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend", | 
					
						
							| 
									
										
										
										
											2021-06-30 20:57:00 +02:00
										 |  |  |             "receiving": receiving_player, | 
					
						
							|  |  |  |             "item": net_item} | 
					
						
							| 
									
										
										
										
											2020-02-22 18:09:25 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | class CommandMeta(type): | 
					
						
							|  |  |  |     def __new__(cls, name, bases, attrs): | 
					
						
							|  |  |  |         commands = attrs["commands"] = {} | 
					
						
							|  |  |  |         for base in bases: | 
					
						
							|  |  |  |             commands.update(base.commands) | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         commands.update({command_name[5:]: method for command_name, method in attrs.items() if | 
					
						
							|  |  |  |                          command_name.startswith("_cmd_")}) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         return super(CommandMeta, cls).__new__(cls, name, bases, attrs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-25 13:54:43 -04:00
										 |  |  | _Return = typing.TypeVar("_Return") | 
					
						
							|  |  |  | # TODO: when python 3.10 is lowest supported, typing.ParamSpec | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def mark_raw(function: typing.Callable[[typing.Any], _Return]) -> typing.Callable[[typing.Any], _Return]: | 
					
						
							| 
									
										
										
										
											2020-04-19 03:24:27 +02:00
										 |  |  |     function.raw_text = True | 
					
						
							|  |  |  |     return function | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | class CommandProcessor(metaclass=CommandMeta): | 
					
						
							|  |  |  |     commands: typing.Dict[str, typing.Callable] | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     client = None | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |     marker = "/" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def output(self, text: str): | 
					
						
							|  |  |  |         print(text) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |     def __call__(self, raw: str) -> typing.Optional[bool]: | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         if not raw: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2020-04-15 08:49:30 +02:00
										 |  |  |         try: | 
					
						
							|  |  |  |             command = raw.split() | 
					
						
							|  |  |  |             basecommand = command[0] | 
					
						
							|  |  |  |             if basecommand[0] == self.marker: | 
					
						
							|  |  |  |                 method = self.commands.get(basecommand[1:].lower(), None) | 
					
						
							|  |  |  |                 if not method: | 
					
						
							|  |  |  |                     self._error_unknown_command(basecommand[1:]) | 
					
						
							|  |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2020-04-19 15:32:27 +02:00
										 |  |  |                     if getattr(method, "raw_text", False):  # method is requesting unprocessed text data | 
					
						
							| 
									
										
										
										
											2020-04-19 15:31:15 +02:00
										 |  |  |                         arg = raw.split(maxsplit=1) | 
					
						
							|  |  |  |                         if len(arg) > 1: | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                             return method(self, arg[1])  # argument text was found, so pass it along | 
					
						
							| 
									
										
										
										
											2020-04-19 15:31:15 +02:00
										 |  |  |                         else: | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                             return method(self)  # argument may be optional, try running without args | 
					
						
							| 
									
										
										
										
											2020-04-19 03:24:27 +02:00
										 |  |  |                     else: | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                         return method(self, *command[1:])  # pass each word as argument | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2020-04-15 08:49:30 +02:00
										 |  |  |                 self.default(raw) | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							|  |  |  |             self._error_parsing_command(e) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def get_help_text(self) -> str: | 
					
						
							|  |  |  |         s = "" | 
					
						
							|  |  |  |         for command, method in self.commands.items(): | 
					
						
							|  |  |  |             spec = inspect.signature(method).parameters | 
					
						
							|  |  |  |             argtext = "" | 
					
						
							|  |  |  |             for argname, parameter in spec.items(): | 
					
						
							|  |  |  |                 if argname == "self": | 
					
						
							|  |  |  |                     continue | 
					
						
							| 
									
										
										
										
											2020-04-15 09:56:28 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                 if isinstance(parameter.default, str): | 
					
						
							| 
									
										
										
										
											2020-04-15 09:56:28 +02:00
										 |  |  |                     if not parameter.default: | 
					
						
							|  |  |  |                         argname = f"[{argname}]" | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         argname += "=" + parameter.default | 
					
						
							|  |  |  |                 argtext += argname | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                 argtext += " " | 
					
						
							|  |  |  |             s += f"{self.marker}{command} {argtext}\n    {method.__doc__}\n" | 
					
						
							|  |  |  |         return s | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_help(self): | 
					
						
							|  |  |  |         """Returns the help listing""" | 
					
						
							|  |  |  |         self.output(self.get_help_text()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_license(self): | 
					
						
							|  |  |  |         """Returns the licensing information""" | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |         license = getattr(CommandProcessor, "license", None) | 
					
						
							|  |  |  |         if not license: | 
					
						
							| 
									
										
										
										
											2020-04-20 21:15:13 +02:00
										 |  |  |             with open(Utils.local_path("LICENSE")) as f: | 
					
						
							| 
									
										
										
										
											2020-07-21 23:15:19 +02:00
										 |  |  |                 CommandProcessor.license = f.read() | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |         self.output(CommandProcessor.license) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def default(self, raw: str): | 
					
						
							|  |  |  |         self.output("Echo: " + raw) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _error_unknown_command(self, raw: str): | 
					
						
							|  |  |  |         self.output(f"Could not find command {raw}. Known commands: {', '.join(self.commands)}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 08:49:30 +02:00
										 |  |  |     def _error_parsing_command(self, exception: Exception): | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |         import traceback | 
					
						
							|  |  |  |         self.output(traceback.format_exc()) | 
					
						
							| 
									
										
										
										
											2020-04-15 08:49:30 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-05 21:45:52 +02:00
										 |  |  | class CommonCommandProcessor(CommandProcessor): | 
					
						
							|  |  |  |     ctx: Context | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_countdown(self, seconds: str = "10") -> bool: | 
					
						
							|  |  |  |         """Start a countdown in seconds""" | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             timer = int(seconds, 10) | 
					
						
							|  |  |  |         except ValueError: | 
					
						
							|  |  |  |             timer = 10 | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(countdown(self.ctx, timer)) | 
					
						
							| 
									
										
										
										
											2020-07-05 21:45:52 +02:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_options(self): | 
					
						
							|  |  |  |         """List all current options. Warning: lists password.""" | 
					
						
							|  |  |  |         self.output("Current options:") | 
					
						
							| 
									
										
										
										
											2020-11-15 15:21:41 +01:00
										 |  |  |         for option in self.ctx.simple_options: | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |             if option == "server_password" and self.marker == "!":  # Do not display the server password to the client. | 
					
						
							|  |  |  |                 self.output(f"Option server_password is set to {('*' * random.randint(4, 16))}") | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output(f"Option {option} is set to {getattr(self.ctx, option)}") | 
					
						
							| 
									
										
										
										
											2020-07-05 21:45:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-11 13:19:38 -07:00
										 |  |  | class ClientMessageProcessor(CommonCommandProcessor): | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |     marker = "!" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, ctx: Context, client: Client): | 
					
						
							|  |  |  |         self.ctx = ctx | 
					
						
							|  |  |  |         self.client = client | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     def __call__(self, raw: str) -> typing.Optional[bool]: | 
					
						
							|  |  |  |         if not raw.startswith("!admin"): | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |             self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + raw, | 
					
						
							|  |  |  |                                         {"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": raw}) | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |         return super(ClientMessageProcessor, self).__call__(raw) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     def output(self, text: str): | 
					
						
							|  |  |  |         self.ctx.notify_client(self.client, text, {"type": "CommandResult"}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def output_multiple(self, texts: typing.List[str]): | 
					
						
							|  |  |  |         self.ctx.notify_client_multiple(self.client, texts, {"type": "CommandResult"}) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:26:22 +02:00
										 |  |  |     def default(self, raw: str): | 
					
						
							|  |  |  |         pass  # default is client sending just text | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     def is_authenticated(self): | 
					
						
							|  |  |  |         return self.ctx.commandprocessor.client == self.client | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_admin(self, command: str = ""): | 
					
						
							| 
									
										
										
										
											2022-05-04 20:03:19 -07:00
										 |  |  |         """Allow remote administration of the multiworld server
 | 
					
						
							|  |  |  |         Usage: "!admin login <password>" in order to log in to the remote interface. | 
					
						
							|  |  |  |         Once logged in, you can then use "!admin <command>" to issue commands. | 
					
						
							|  |  |  |         If you need further help once logged in.  use "!admin /help" """
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         output = f"!admin {command}" | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |         if output.lower().startswith( | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |                 "!admin login"):  # disallow others from seeing the supplied password, whether it is correct. | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |             output = f"!admin login {('*' * random.randint(4, 16))}" | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |         elif output.lower().startswith( | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |                 # disallow others from knowing what the new remote administration password is. | 
					
						
							|  |  |  |                 "!admin /option server_password"): | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |             output = f"!admin /option server_password {('*' * random.randint(4, 16))}" | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         self.ctx.broadcast_text_all(self.ctx.get_aliased_name(self.client.team, self.client.slot) + ': ' + output, | 
					
						
							|  |  |  |                                     {"type": "Chat", "team": self.client.team, "slot": self.client.slot, "message": output}) | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if not self.ctx.server_password: | 
					
						
							|  |  |  |             self.output("Sorry, Remote administration is disabled") | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not command: | 
					
						
							|  |  |  |             if self.is_authenticated(): | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |                 self.output("Usage: !admin [Server command].\nUse !admin /help for help.\n" | 
					
						
							|  |  |  |                             "Use !admin logout to log out of the current session.") | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output("Usage: !admin login [password]") | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if command.startswith("login "): | 
					
						
							|  |  |  |             if command == f"login {self.ctx.server_password}": | 
					
						
							|  |  |  |                 self.output("Login successful. You can now issue server side commands.") | 
					
						
							|  |  |  |                 self.ctx.commandprocessor.client = self.client | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output("Password incorrect.") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not self.is_authenticated(): | 
					
						
							|  |  |  |             self.output("You must first login using !admin login [password]") | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if command == "logout": | 
					
						
							|  |  |  |             self.output("Logout successful. You can no longer issue server side commands.") | 
					
						
							|  |  |  |             self.ctx.commandprocessor.client = None | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return self.ctx.commandprocessor(command) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |     def _cmd_players(self) -> bool: | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |         """Get information about connected and missing players.""" | 
					
						
							| 
									
										
										
										
											2020-04-20 23:03:52 +02:00
										 |  |  |         if len(self.ctx.player_names) < 10: | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |             self.ctx.broadcast_text_all(get_players_string(self.ctx), {"type": "CommandResult"}) | 
					
						
							| 
									
										
										
										
											2020-04-20 23:03:52 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output(get_players_string(self.ctx)) | 
					
						
							| 
									
										
										
										
											2020-04-24 05:29:02 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  |     def _cmd_status(self, tag:str="") -> bool: | 
					
						
							|  |  |  |         """Get status information about your team.
 | 
					
						
							|  |  |  |         Optionally mention a Tag name and get information on who has that Tag. | 
					
						
							|  |  |  |         For example: DeathLink or EnergyLink."""
 | 
					
						
							|  |  |  |         self.output(get_status_string(self.ctx, self.client.team, tag)) | 
					
						
							| 
									
										
										
										
											2021-11-04 08:57:27 +01:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |     def _cmd_release(self) -> bool: | 
					
						
							|  |  |  |         """Sends remaining items in your world to their recipients.""" | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |         if self.ctx.allow_releases.get((self.client.team, self.client.slot), False): | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |             release_player(self.ctx, self.client.team, self.client.slot) | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |         if "enabled" in self.ctx.release_mode: | 
					
						
							|  |  |  |             release_player(self.ctx, self.client.team, self.client.slot) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |         elif "disabled" in self.ctx.release_mode: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             self.output("Sorry, client item releasing has been disabled on this server. " | 
					
						
							|  |  |  |                         "You can ask the server admin for a /release") | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |         else:  # is auto or goal | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                 release_player(self.ctx, self.client.team, self.client.slot) | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output( | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |                     "Sorry, client item releasing requires you to have beaten the game on this server." | 
					
						
							|  |  |  |                     " You can ask the server admin for a /release") | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-19 01:47:11 +02:00
										 |  |  |     def _cmd_collect(self) -> bool: | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         """Send your remaining items to yourself""" | 
					
						
							|  |  |  |         if "enabled" in self.ctx.collect_mode: | 
					
						
							|  |  |  |             collect_player(self.ctx, self.client.team, self.client.slot) | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         elif "disabled" in self.ctx.collect_mode: | 
					
						
							|  |  |  |             self.output( | 
					
						
							|  |  |  |                 "Sorry, client collecting has been disabled on this server. You can ask the server admin for a /collect") | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         else:  # is auto or goal | 
					
						
							|  |  |  |             if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: | 
					
						
							|  |  |  |                 collect_player(self.ctx, self.client.team, self.client.slot) | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output( | 
					
						
							|  |  |  |                     "Sorry, client collecting requires you to have beaten the game on this server." | 
					
						
							|  |  |  |                     " You can ask the server admin for a /collect") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |     def _cmd_remaining(self) -> bool: | 
					
						
							|  |  |  |         """List remaining items in your game, but not their location or recipient""" | 
					
						
							|  |  |  |         if self.ctx.remaining_mode == "enabled": | 
					
						
							|  |  |  |             remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) | 
					
						
							|  |  |  |             if remaining_item_ids: | 
					
						
							| 
									
										
										
										
											2024-07-03 06:39:08 -07:00
										 |  |  |                 self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                                                             for item_id in remaining_item_ids)) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output("No remaining items found.") | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         elif self.ctx.remaining_mode == "disabled": | 
					
						
							|  |  |  |             self.output( | 
					
						
							|  |  |  |                 "Sorry, !remaining has been disabled on this server.") | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  |         else:  # is goal | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                 remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) | 
					
						
							|  |  |  |                 if remaining_item_ids: | 
					
						
							| 
									
										
										
										
											2024-07-03 06:39:08 -07:00
										 |  |  |                     self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                                                                 for item_id in remaining_item_ids)) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     self.output("No remaining items found.") | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output( | 
					
						
							|  |  |  |                     "Sorry, !remaining requires you to have beaten the game on this server") | 
					
						
							|  |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-14 20:22:12 +02:00
										 |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |     def _cmd_missing(self, filter_text="") -> bool: | 
					
						
							|  |  |  |         """List all missing location checks from the server's perspective.
 | 
					
						
							|  |  |  |         Can be given text, which will be used as filter."""
 | 
					
						
							| 
									
										
										
										
											2020-06-15 22:07:43 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |         locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) | 
					
						
							| 
									
										
										
										
											2020-04-20 11:47:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         if locations: | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |             game = self.ctx.slot_info[self.client.slot].game | 
					
						
							|  |  |  |             names = [self.ctx.location_names[game][location] for location in locations] | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |             if filter_text: | 
					
						
							| 
									
										
										
										
											2024-04-14 20:22:12 +02:00
										 |  |  |                 location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] | 
					
						
							|  |  |  |                 if filter_text in location_groups:  # location group name | 
					
						
							|  |  |  |                     names = [name for name in names if name in location_groups[filter_text]] | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     names = [name for name in names if filter_text in name] | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |             texts = [f'Missing: {name}' for name in names] | 
					
						
							|  |  |  |             if filter_text: | 
					
						
							|  |  |  |                 texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.") | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 texts.append(f"Found {len(locations)} missing location checks") | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |             self.output_multiple(texts) | 
					
						
							| 
									
										
										
										
											2020-04-20 11:47:50 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output("No missing location checks found.") | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2020-04-20 11:47:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-14 20:22:12 +02:00
										 |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |     def _cmd_checked(self, filter_text="") -> bool: | 
					
						
							|  |  |  |         """List all done location checks from the server's perspective.
 | 
					
						
							|  |  |  |         Can be given text, which will be used as filter."""
 | 
					
						
							| 
									
										
										
										
											2021-10-20 19:58:07 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if locations: | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |             game = self.ctx.slot_info[self.client.slot].game | 
					
						
							|  |  |  |             names = [self.ctx.location_names[game][location] for location in locations] | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |             if filter_text: | 
					
						
							| 
									
										
										
										
											2024-04-14 20:22:12 +02:00
										 |  |  |                 location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]] | 
					
						
							|  |  |  |                 if filter_text in location_groups:  # location group name | 
					
						
							|  |  |  |                     names = [name for name in names if name in location_groups[filter_text]] | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     names = [name for name in names if filter_text in name] | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |             texts = [f'Checked: {name}' for name in names] | 
					
						
							|  |  |  |             if filter_text: | 
					
						
							|  |  |  |                 texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.") | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 texts.append(f"Found {len(locations)} done location checks") | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |             self.output_multiple(texts) | 
					
						
							| 
									
										
										
										
											2021-10-20 19:58:07 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output("No done location checks found.") | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_alias(self, alias_name: str = ""): | 
					
						
							| 
									
										
										
										
											2020-06-10 06:13:14 +02:00
										 |  |  |         """Set your alias to the passed name.""" | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |         if alias_name: | 
					
						
							| 
									
										
										
										
											2020-06-10 06:13:14 +02:00
										 |  |  |             alias_name = alias_name[:16].strip() | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |             self.ctx.name_aliases[self.client.team, self.client.slot] = alias_name | 
					
						
							|  |  |  |             self.output(f"Hello, {alias_name}") | 
					
						
							|  |  |  |             update_aliases(self.ctx, self.client.team) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |             self.ctx.save() | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  |         elif (self.client.team, self.client.slot) in self.ctx.name_aliases: | 
					
						
							|  |  |  |             del (self.ctx.name_aliases[self.client.team, self.client.slot]) | 
					
						
							|  |  |  |             self.output("Removed Alias") | 
					
						
							|  |  |  |             update_aliases(self.ctx, self.client.team) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |             self.ctx.save() | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |             return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-19 03:24:27 +02:00
										 |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |     def _cmd_getitem(self, item_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2020-06-10 06:13:14 +02:00
										 |  |  |         """Cheat in an item, if it is enabled on this server""" | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         if self.ctx.item_cheat: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot]) | 
					
						
							|  |  |  |             item_name, usable, response = get_intended_text( | 
					
						
							|  |  |  |                 item_name, | 
					
						
							|  |  |  |                 names | 
					
						
							|  |  |  |             ) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             if usable: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 new_item = NetworkItem(names[item_name], -1, self.client.slot) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                 get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) | 
					
						
							|  |  |  |                 get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |                 self.ctx.broadcast_text_all( | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |                     'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |                                                                                                  self.client.slot), | 
					
						
							|  |  |  |                     {"type": "ItemCheat", "team": self.client.team, "receiving": self.client.slot, "item": new_item}) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                 send_new_items(self.ctx) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                 return True | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output("Cheating is disabled.") | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |     def get_hints(self, input_text: str, for_location: bool = False) -> bool: | 
					
						
							| 
									
										
										
										
											2020-06-15 06:30:51 +02:00
										 |  |  |         points_available = get_client_points(self.ctx, self.client) | 
					
						
							| 
									
										
										
										
											2022-10-28 02:45:18 -07:00
										 |  |  |         cost = self.ctx.get_hint_cost(self.client.slot) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-09 20:47:12 -07:00
										 |  |  |         if not input_text: | 
					
						
							| 
									
										
										
										
											2020-04-22 05:09:46 +02:00
										 |  |  |             hints = {hint.re_check(self.ctx, self.client.team) for hint in | 
					
						
							|  |  |  |                      self.ctx.hints[self.client.team, self.client.slot]} | 
					
						
							|  |  |  |             self.ctx.hints[self.client.team, self.client.slot] = hints | 
					
						
							| 
									
										
										
										
											2024-03-03 06:34:48 +01:00
										 |  |  |             self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,)) | 
					
						
							| 
									
										
										
										
											2021-08-01 16:48:25 +02:00
										 |  |  |             self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. " | 
					
						
							|  |  |  |                         f"You have {points_available} points.") | 
					
						
							| 
									
										
										
										
											2024-03-03 06:34:48 +01:00
										 |  |  |             if hints and Utils.version_tuple < (0, 5, 0): | 
					
						
							|  |  |  |                 self.output("It was recently changed, so that the above hints are only shown to you. " | 
					
						
							|  |  |  |                             "If you meant to alert another player of an above hint, " | 
					
						
							|  |  |  |                             "please let them know of the content or to run !hint themselves.") | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         elif input_text.isnumeric(): | 
					
						
							|  |  |  |             game = self.ctx.games[self.client.slot] | 
					
						
							|  |  |  |             hint_id = int(input_text) | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |             hint_name = self.ctx.item_names[game][hint_id] \ | 
					
						
							|  |  |  |                 if not for_location and hint_id in self.ctx.item_names[game] \ | 
					
						
							|  |  |  |                 else self.ctx.location_names[game][hint_id] \ | 
					
						
							|  |  |  |                 if for_location and hint_id in self.ctx.location_names[game] \ | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 else None | 
					
						
							|  |  |  |             if hint_name in self.ctx.non_hintable_names[game]: | 
					
						
							|  |  |  |                 self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") | 
					
						
							|  |  |  |                 hints = [] | 
					
						
							|  |  |  |             elif not for_location: | 
					
						
							|  |  |  |                 hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             game = self.ctx.games[self.client.slot] | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             if game not in self.ctx.all_item_and_group_names: | 
					
						
							|  |  |  |                 self.output("Can't look up item/location for unknown game. Hint for ID instead.") | 
					
						
							|  |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |             names = self.ctx.all_location_and_group_names[game] \ | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 if for_location else \ | 
					
						
							|  |  |  |                 self.ctx.all_item_and_group_names[game] | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             hint_name, usable, response = get_intended_text(input_text, names) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             if usable: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 if hint_name in self.ctx.non_hintable_names[game]: | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |                     self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                     hints = [] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 elif not for_location and hint_name in self.ctx.item_name_groups[game]:  # item group name | 
					
						
							| 
									
										
										
										
											2020-06-07 00:19:19 +02:00
										 |  |  |                     hints = [] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                     for item_name in self.ctx.item_name_groups[game][hint_name]: | 
					
						
							|  |  |  |                         if item_name in self.ctx.item_names_for_game(game):  # ensure item has an ID | 
					
						
							|  |  |  |                             hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) | 
					
						
							|  |  |  |                 elif not for_location and hint_name in self.ctx.item_names_for_game(game):  # item name | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |                     hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) | 
					
						
							| 
									
										
										
										
											2023-03-08 15:15:28 -06:00
										 |  |  |                 elif hint_name in self.ctx.location_name_groups[game]:  # location group name | 
					
						
							|  |  |  |                     hints = [] | 
					
						
							|  |  |  |                     for loc_name in self.ctx.location_name_groups[game][hint_name]: | 
					
						
							|  |  |  |                         if loc_name in self.ctx.location_names_for_game(game): | 
					
						
							|  |  |  |                             hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name)) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                 else:  # location name | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  |                     hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) | 
					
						
							| 
									
										
										
										
											2020-06-07 02:13:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |         if hints: | 
					
						
							|  |  |  |             new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] | 
					
						
							| 
									
										
										
										
											2024-05-03 22:28:09 -04:00
										 |  |  |             old_hints = list(set(hints) - new_hints) | 
					
						
							|  |  |  |             if old_hints and not new_hints: | 
					
						
							|  |  |  |                 self.ctx.notify_hints(self.client.team, old_hints) | 
					
						
							|  |  |  |                 self.output("Hint was previously used, no points deducted.") | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             if new_hints: | 
					
						
							|  |  |  |                 found_hints = [hint for hint in new_hints if hint.found] | 
					
						
							|  |  |  |                 not_found_hints = [hint for hint in new_hints if not hint.found] | 
					
						
							|  |  |  |                 if not not_found_hints:  # everything's been found, no need to pay | 
					
						
							|  |  |  |                     can_pay = 1000 | 
					
						
							|  |  |  |                 elif cost: | 
					
						
							|  |  |  |                     can_pay = int((points_available // cost) > 0)  # limit to 1 new hint per call | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     can_pay = 1000 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 self.ctx.random.shuffle(not_found_hints) | 
					
						
							|  |  |  |                 # By popular vote, make hints prefer non-local placements | 
					
						
							|  |  |  |                 not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |                 # By another popular vote, prefer early sphere | 
					
						
							|  |  |  |                 not_found_hints.sort(key=lambda hint: self.ctx.get_sphere(hint.finding_player, hint.location), | 
					
						
							|  |  |  |                                      reverse=True) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 22:28:09 -04:00
										 |  |  |                 hints = found_hints + old_hints | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 while can_pay > 0: | 
					
						
							|  |  |  |                     if not not_found_hints: | 
					
						
							|  |  |  |                         break | 
					
						
							|  |  |  |                     hint = not_found_hints.pop() | 
					
						
							|  |  |  |                     hints.append(hint) | 
					
						
							|  |  |  |                     can_pay -= 1 | 
					
						
							|  |  |  |                     self.ctx.hints_used[self.client.team, self.client.slot] += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-03 22:28:09 -04:00
										 |  |  |                 self.ctx.notify_hints(self.client.team, hints) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 if not_found_hints: | 
					
						
							| 
									
										
										
										
											2024-05-27 18:43:25 +02:00
										 |  |  |                     points_available = get_client_points(self.ctx, self.client) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                     if hints and cost and int((points_available // cost) == 0): | 
					
						
							|  |  |  |                         self.output( | 
					
						
							|  |  |  |                             f"There may be more hintables, however, you cannot afford to pay for any more. " | 
					
						
							|  |  |  |                             f" You have {points_available} and need at least " | 
					
						
							|  |  |  |                             f"{self.ctx.get_hint_cost(self.client.slot)}.") | 
					
						
							|  |  |  |                     elif hints: | 
					
						
							|  |  |  |                         self.output( | 
					
						
							|  |  |  |                             "There may be more hintables, you can rerun the command to find more.") | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         self.output(f"You can't afford the hint. " | 
					
						
							|  |  |  |                                     f"You have {points_available} points and need at least " | 
					
						
							|  |  |  |                                     f"{self.ctx.get_hint_cost(self.client.slot)}.") | 
					
						
							|  |  |  |                 self.ctx.save() | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2022-10-28 02:45:18 -07:00
										 |  |  |             if points_available >= cost: | 
					
						
							| 
									
										
										
										
											2022-12-05 02:06:13 +01:00
										 |  |  |                 if for_location: | 
					
						
							|  |  |  |                     self.output(f"Nothing found for recognized location name \"{hint_name}\". " | 
					
						
							|  |  |  |                                 f"Location appears to not exist in this multiworld.") | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     self.output(f"Nothing found for recognized item name \"{hint_name}\". " | 
					
						
							|  |  |  |                                 f"Item appears to not exist in this multiworld.") | 
					
						
							| 
									
										
										
										
											2022-10-28 02:45:18 -07:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output(f"You can't afford the hint. " | 
					
						
							|  |  |  |                             f"You have {points_available} points and need at least " | 
					
						
							|  |  |  |                             f"{self.ctx.get_hint_cost(self.client.slot)}.") | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-09 20:47:12 -07:00
										 |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _cmd_hint(self, item_name: str = "") -> bool: | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |         """Use !hint {item_name},
 | 
					
						
							|  |  |  |         for example !hint Lamp to get a spoiler peek for that item. | 
					
						
							| 
									
										
										
										
											2021-10-09 20:47:12 -07:00
										 |  |  |         If hint costs are on, this will only give you one new result, | 
					
						
							|  |  |  |         you can rerun the command to get more in that case."""
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |         return self.get_hints(item_name) | 
					
						
							| 
									
										
										
										
											2021-10-09 20:47:12 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_hint_location(self, location: str = "") -> bool: | 
					
						
							|  |  |  |         """Use !hint_location {location_name},
 | 
					
						
							| 
									
										
										
										
											2022-01-18 06:16:16 +01:00
										 |  |  |         for example !hint_location atomic-bomb to get a spoiler peek for that location."""
 | 
					
						
							| 
									
										
										
										
											2021-10-09 20:47:12 -07:00
										 |  |  |         return self.get_hints(location, True) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-15 03:22:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]: | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     return ctx.locations.get_checked(ctx.location_checks, team, slot) | 
					
						
							| 
									
										
										
										
											2021-01-21 16:21:51 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]: | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     return ctx.locations.get_missing(ctx.location_checks, team, slot) | 
					
						
							| 
									
										
										
										
											2021-01-15 03:22:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-15 06:30:51 +02:00
										 |  |  | def get_client_points(ctx: Context, client: Client) -> int: | 
					
						
							|  |  |  |     return (ctx.location_check_points * len(ctx.location_checks[client.team, client.slot]) - | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |             ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot]) | 
					
						
							| 
									
										
										
										
											2020-06-15 06:30:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-15 03:22:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  | def get_slot_points(ctx: Context, team: int, slot: int) -> int: | 
					
						
							|  |  |  |     return (ctx.location_check_points * len(ctx.location_checks[team, slot]) - | 
					
						
							|  |  |  |             ctx.get_hint_cost(slot) * ctx.hints_used[team, slot]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | async def process_client_cmd(ctx: Context, client: Client, args: dict): | 
					
						
							| 
									
										
										
										
											2021-02-21 23:54:08 +01:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |         cmd: str = args["cmd"] | 
					
						
							| 
									
										
										
										
											2021-02-21 23:54:08 +01:00
										 |  |  |     except: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         ctx.logger.exception(f"Could not get command from {args}") | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |         await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None, | 
					
						
							| 
									
										
										
										
											2021-07-14 10:02:39 +02:00
										 |  |  |                                       "text": f"Could not get command from {args} at `cmd`"}]) | 
					
						
							| 
									
										
										
										
											2021-02-21 23:54:08 +01:00
										 |  |  |         raise | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     if type(cmd) is not str: | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |         await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None, | 
					
						
							| 
									
										
										
										
											2021-07-14 10:02:39 +02:00
										 |  |  |                                       "text": f"Command should be str, got {type(cmd)}"}]) | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     if cmd == 'Connect': | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |         if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \ | 
					
						
							|  |  |  |                 'game' not in args: | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |             await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect', | 
					
						
							|  |  |  |                                           "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         errors = set() | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |         if ctx.password and args['password'] != ctx.password: | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |             errors.add('InvalidPassword') | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |         if args['name'] not in ctx.connect_names: | 
					
						
							|  |  |  |             errors.add('InvalidSlot') | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |             team, slot = ctx.connect_names[args['name']] | 
					
						
							| 
									
										
										
										
											2021-04-10 21:08:01 +02:00
										 |  |  |             game = ctx.games[slot] | 
					
						
							| 
									
										
										
										
											2024-05-02 03:38:49 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |             ignore_game = not args.get("game") and any(tag in _non_game_messages for tag in args["tags"]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  |             if not ignore_game and args['game'] != game: | 
					
						
							| 
									
										
										
										
											2021-09-17 04:32:09 +02:00
										 |  |  |                 errors.add('InvalidGame') | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  |             minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot] | 
					
						
							| 
									
										
										
										
											2021-11-22 20:32:59 +01:00
										 |  |  |             if minver > args['version']: | 
					
						
							|  |  |  |                 errors.add('IncompatibleVersion') | 
					
						
							| 
									
										
										
										
											2022-12-11 02:59:17 +01:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 client.items_handling = args['items_handling'] | 
					
						
							|  |  |  |             except (ValueError, TypeError): | 
					
						
							|  |  |  |                 errors.add('InvalidItemsHandling') | 
					
						
							| 
									
										
										
										
											2020-12-29 19:23:14 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |         # only exact version match allowed | 
					
						
							| 
									
										
										
										
											2021-06-18 22:15:54 +02:00
										 |  |  |         if ctx.compatibility == 0 and args['version'] != version_tuple: | 
					
						
							| 
									
										
										
										
											2020-07-16 16:57:38 +02:00
										 |  |  |             errors.add('IncompatibleVersion') | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         if errors: | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |             ctx.logger.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.") | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |             await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |             team, slot = ctx.connect_names[args['name']] | 
					
						
							| 
									
										
										
										
											2021-10-25 06:57:06 +02:00
										 |  |  |             if client.auth and client.team is not None and client.slot in ctx.clients[client.team]: | 
					
						
							|  |  |  |                 ctx.clients[team][slot].remove(client)  # re-auth, remove old entry | 
					
						
							|  |  |  |                 if client.team != team or client.slot != slot: | 
					
						
							|  |  |  |                     client.auth = False  # swapping Team/Slot | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |             client.team = team | 
					
						
							|  |  |  |             client.slot = slot | 
					
						
							| 
									
										
										
										
											2021-11-22 20:32:59 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |             ctx.client_ids[client.team, client.slot] = args["uuid"] | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |             ctx.clients[team][slot].append(client) | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |             client.version = args['version'] | 
					
						
							|  |  |  |             client.tags = args['tags'] | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             connected_packet = { | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |                 "cmd": "Connected", | 
					
						
							|  |  |  |                 "team": client.team, "slot": client.slot, | 
					
						
							|  |  |  |                 "players": ctx.get_players_package(), | 
					
						
							| 
									
										
										
										
											2021-10-20 05:56:28 +02:00
										 |  |  |                 "missing_locations": get_missing_checks(ctx, team, slot), | 
					
						
							|  |  |  |                 "checked_locations": get_checked_checks(ctx, team, slot), | 
					
						
							| 
									
										
										
										
											2023-04-10 14:44:20 -05:00
										 |  |  |                 "slot_info": ctx.slot_info, | 
					
						
							|  |  |  |                 "hint_points": get_slot_points(ctx, team, slot), | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |             reply = [connected_packet] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |             items = get_received_items(ctx, client.team, client.slot, client.remote_items) | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             if (start_inventory or items) and not client.no_items: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                 reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items}) | 
					
						
							|  |  |  |                 client.send_index = len(start_inventory) + len(items) | 
					
						
							| 
									
										
										
										
											2021-10-25 06:57:06 +02:00
										 |  |  |             if not client.auth:  # if this was a Re-Connect, don't print to console | 
					
						
							|  |  |  |                 client.auth = True | 
					
						
							| 
									
										
										
										
											2021-10-25 08:24:32 +02:00
										 |  |  |                 await on_client_joined(ctx, client) | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             if args.get("slot_data", True): | 
					
						
							|  |  |  |                 connected_packet["slot_data"] = ctx.slot_data[client.slot] | 
					
						
							| 
									
										
											  
											
												WebUI (#100)
* Object-Oriented base changes for web-ui prep
* remove debug raise
* optimize broadcast to serialize once
* Implement WebUI socket, static assets, and classes
- Still need to wrap logging functions and send output to UI
- UI commands are successfully being sent to the server
* GUI operational. Wrap logging functions, implement server address selection on GUI, automatically launch web browser when client websocket is served
* Update MultiServer status when a user disconnects / reconnects
* Implement colored item and hint checks, improve GUI readability
* Fix improper formatting on received items
* Update SNES connection status on disconnect / reconnect. Implement itemFound, prevent accidentally printing JS objects
* Minor text change for itemFound
* Fixed a very wrong comment
* Fixed client commands not working, fixed un-helpful error messages appearing in GUI
* Fix a bug causing a failure to connect to a multiworld server if a previously existing cached address was present and the client was loaded without an address passed in
* Convert WebUI to React /w Redux. WebSocket communications not yet operational.
* WebUI fully converted to React / Redux.
- Websocket communication operational
- Added a button to connect to the multiserver which appears only when a SNES is connected and a server connection is not active
* Restore some features lost in WebUI
- Restore (found) notification on hints if the item has already been obtained
- Restore (x/y) indicator on received items, which indicates the number of items the client is waiting to receive from the client in a queue
* Fix a grammatical UI big causing player names to show only an apostrophe when possessive
* Add support for multiple SNES Devices, and switching between them
* freeze support for client
* make sure flask works when frozen
* UI Improvements
- Hint messages now actually show a found status via ✔ and ❌ emoji
- Active player name is always a different color than other players (orange for now)
- Add a toggle to show only entries relevant to the active player
- Added a WidgetArea
- Added a notes widget
* Received items now marked as relevant
* Include production build for deployment
* Notes now survive a browser close. Minimum width applied to monitor to prevent CSS issues.
* include webUi folder in setup.py
* Bugfixes for Monitor
- Fix a bug causing the monitor window to grow beyond it's intended content limit
- Reduced monitor content limit to 200 items
- Ensured each monitor entry has a unique key
* Prevent eslint from yelling at me about stupid things
* Add button to collapse sidebar, press enter on empty server input to disconnect on purpose
* WebUI is now aware of client disconnect, message log limit increased to 350, fix !missing output
* Update WebUI to v2.2.1
- Added color to WebUI for entrance-span
- Make !missing show total count at bottom of list to match /missing behavior
* Fix a bug causing clients version <= 2.2.0 to crash when anyone asks for a hint
- Also fix a bug in the WebUI causing the entrance location to always show as "somewhere"
* Update WebUI color palette (this cost me $50)
* allow text console input alongside web-ui
* remove Flask
a bit overkill for what we're doing
* remove jinja2
* Update WebUI to work with new hosting mechanism
* with flask gone, we no longer need subprocess shenanigans
* If multiple web ui clients try to run, at least present a working console
* Update MultiClient and WebUI to handle multiple clients simultaneously.
- The port on which the websocket for the WebUI is hosted is not chosen randomly from 5000 - 5999. This port is passed to the browser so it knows which MultiClient to connect to
- Removed failure condition if a web server is already running, as there is no need to run more than one web server on a single system. If an exception is thrown while attempting to launch a web server, a check is made for the port being unavailable. If the port is unavailable, it probably means the user is launching a second MultiClient. A web browser is then opened with a connection to the correct webui_socket_port.
- Add a /web command to the MultiClient to repoen the appropriate browser window and get params in case a user accidentally closes the tab
* Use proper name for WebUI
* move webui into /data with other data files
* make web ui optional
This is mostly for laptop users wanting to preserve some battery, should not be needed outside of that.
* fix direct server start
* re-add connection timer
* fix indentation
Co-authored-by: Chris <chris@legendserver.info>
											
										 
											2020-06-03 21:29:43 +02:00
										 |  |  |             await ctx.send_msgs(client, reply) | 
					
						
							| 
									
										
										
										
											2021-10-25 06:57:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-25 02:07:28 +01:00
										 |  |  |     elif cmd == "GetDataPackage": | 
					
						
							| 
									
										
										
										
											2021-11-21 18:11:51 +01:00
										 |  |  |         exclusions = args.get("exclusions", []) | 
					
						
							| 
									
										
										
										
											2022-04-30 04:39:08 +02:00
										 |  |  |         if "games" in args: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             games = {name: game_data for name, game_data in ctx.gamespackage.items() | 
					
						
							| 
									
										
										
										
											2022-04-30 04:39:08 +02:00
										 |  |  |                      if name in set(args.get("games", []))} | 
					
						
							|  |  |  |             await ctx.send_msgs(client, [{"cmd": "DataPackage", | 
					
						
							|  |  |  |                                           "data": {"games": games}}]) | 
					
						
							|  |  |  |         # TODO: remove exclusions behaviour around 0.5.0 | 
					
						
							|  |  |  |         elif exclusions: | 
					
						
							| 
									
										
										
										
											2021-11-21 18:11:51 +01:00
										 |  |  |             exclusions = set(exclusions) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             games = {name: game_data for name, game_data in ctx.gamespackage.items() | 
					
						
							| 
									
										
										
										
											2021-07-14 10:35:00 +02:00
										 |  |  |                      if name not in exclusions} | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             package = {"games": games} | 
					
						
							| 
									
										
										
										
											2021-07-14 10:35:00 +02:00
										 |  |  |             await ctx.send_msgs(client, [{"cmd": "DataPackage", | 
					
						
							|  |  |  |                                           "data": package}]) | 
					
						
							| 
									
										
										
										
											2022-04-30 04:39:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-14 10:35:00 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             await ctx.send_msgs(client, [{"cmd": "DataPackage", | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                                           "data": {"games": ctx.gamespackage}}]) | 
					
						
							| 
									
										
										
										
											2021-11-08 16:34:54 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-25 02:07:28 +01:00
										 |  |  |     elif client.auth: | 
					
						
							| 
									
										
										
										
											2021-11-01 20:00:55 +01:00
										 |  |  |         if cmd == "ConnectUpdate": | 
					
						
							|  |  |  |             if not args: | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': cmd, | 
					
						
							|  |  |  |                                               "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2021-11-01 20:00:55 +01:00
										 |  |  |                 return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             if args.get('items_handling', None) is not None and client.items_handling != args['items_handling']: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     client.items_handling = args['items_handling'] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                     start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                     items = get_received_items(ctx, client.team, client.slot, client.remote_items) | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |                     if (items or start_inventory) and not client.no_items: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                         client.send_index = len(start_inventory) + len(items) | 
					
						
							|  |  |  |                         await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0, | 
					
						
							|  |  |  |                                                       "items": start_inventory + items}]) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         client.send_index = 0 | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |                 except (ValueError, TypeError) as err: | 
					
						
							|  |  |  |                     await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', 'type': 'arguments', | 
					
						
							|  |  |  |                                                   'text': f'Invalid items_handling: {err}', | 
					
						
							|  |  |  |                                                   'original_cmd': cmd}]) | 
					
						
							|  |  |  |                     return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if "tags" in args: | 
					
						
							|  |  |  |                 old_tags = client.tags | 
					
						
							|  |  |  |                 client.tags = args["tags"] | 
					
						
							| 
									
										
										
										
											2021-11-08 16:58:41 +01:00
										 |  |  |                 if set(old_tags) != set(client.tags): | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |                     client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |                     ctx.broadcast_text_all( | 
					
						
							| 
									
										
										
										
											2021-11-08 16:58:41 +01:00
										 |  |  |                         f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags " | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |                         f"from {old_tags} to {client.tags}.", | 
					
						
							|  |  |  |                         {"type": "TagsChanged", "team": client.team, "slot": client.slot, "tags": client.tags}) | 
					
						
							| 
									
										
										
										
											2021-11-01 20:00:55 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         elif cmd == 'Sync': | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |             start_inventory = get_start_inventory(ctx, client.slot, client.remote_start_inventory) | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |             items = get_received_items(ctx, client.team, client.slot, client.remote_items) | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             if (start_inventory or items) and not client.no_items: | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                 client.send_index = len(start_inventory) + len(items) | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  |                 await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0, | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |                                               "items": start_inventory + items}]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         elif cmd == 'LocationChecks': | 
					
						
							| 
									
										
										
										
											2022-01-21 02:51:25 +01:00
										 |  |  |             if client.no_locations: | 
					
						
							| 
									
										
										
										
											2021-11-04 13:23:13 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |                                               "text": "Trackers can't register new Location Checks", | 
					
						
							|  |  |  |                                               "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2021-11-04 13:23:13 +01:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 register_location_checks(ctx, client.team, client.slot, args["locations"]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |         elif cmd == 'LocationScouts': | 
					
						
							|  |  |  |             locs = [] | 
					
						
							| 
									
										
										
										
											2022-06-08 04:40:35 +02:00
										 |  |  |             create_as_hint: int = int(args.get("create_as_hint", 0)) | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  |             hints = [] | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |             for location in args["locations"]: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 if type(location) is not int: | 
					
						
							| 
									
										
										
										
											2021-10-09 15:24:08 +02:00
										 |  |  |                     await ctx.send_msgs(client, | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |                                         [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', | 
					
						
							|  |  |  |                                           "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                     return | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |                 target_item, target_player, flags = ctx.locations[client.slot][location] | 
					
						
							| 
									
										
										
										
											2022-01-31 10:11:39 +01:00
										 |  |  |                 if create_as_hint: | 
					
						
							|  |  |  |                     hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |                 locs.append(NetworkItem(target_item, location, target_player, flags)) | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) | 
					
						
							| 
									
										
										
										
											2023-04-08 08:20:59 +02:00
										 |  |  |             if locs and create_as_hint: | 
					
						
							|  |  |  |                 ctx.save() | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |             await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) | 
					
						
							| 
									
										
										
										
											2020-01-18 09:50:12 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |         elif cmd == 'StatusUpdate': | 
					
						
							| 
									
										
										
										
											2021-03-07 22:05:07 +01:00
										 |  |  |             update_client_status(ctx, client, args["status"]) | 
					
						
							| 
									
										
										
										
											2020-04-24 20:07:28 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-02 01:35:24 +02:00
										 |  |  |         elif cmd == 'Say': | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |             if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): | 
					
						
							| 
									
										
										
										
											2021-11-08 16:07:29 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say', | 
					
						
							|  |  |  |                                               "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |                 return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  |             client.messageprocessor(args["text"]) | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-02 01:35:24 +02:00
										 |  |  |         elif cmd == "Bounce": | 
					
						
							|  |  |  |             games = set(args.get("games", [])) | 
					
						
							|  |  |  |             tags = set(args.get("tags", [])) | 
					
						
							|  |  |  |             slots = set(args.get("slots", [])) | 
					
						
							|  |  |  |             args["cmd"] = "Bounced" | 
					
						
							|  |  |  |             msg = ctx.dumper([args]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for bounceclient in ctx.endpoints: | 
					
						
							|  |  |  |                 if client.team == bounceclient.team and (ctx.games[bounceclient.slot] in games or | 
					
						
							|  |  |  |                                                          set(bounceclient.tags) & tags or | 
					
						
							|  |  |  |                                                          bounceclient.slot in slots): | 
					
						
							|  |  |  |                     await ctx.send_encoded_msgs(bounceclient, msg) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |         elif cmd == "Get": | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |             if "keys" not in args or type(args["keys"]) != list: | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", | 
					
						
							|  |  |  |                                               "text": 'Retrieve', "original_cmd": cmd}]) | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |             args["cmd"] = "Retrieved" | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |             keys = args["keys"] | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             args["keys"] = { | 
					
						
							|  |  |  |                 key: ctx.read_data.get(key[6:], lambda: None)() if key.startswith("_read_") else | 
					
						
							|  |  |  |                      ctx.stored_data.get(key, None) | 
					
						
							|  |  |  |                 for key in keys | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |             await ctx.send_msgs(client, [args]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |         elif cmd == "Set": | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             if "key" not in args or args["key"].startswith("_read_") or \ | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |                     "operations" not in args or not type(args["operations"]) == list: | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |                                               "text": 'Set', "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |             args["cmd"] = "SetReply" | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |             value = ctx.stored_data.get(args["key"], args.get("default", 0)) | 
					
						
							| 
									
										
										
										
											2023-01-24 21:14:46 -08:00
										 |  |  |             args["original_value"] = copy.copy(value) | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |             for operation in args["operations"]: | 
					
						
							|  |  |  |                 func = modify_functions[operation["operation"]] | 
					
						
							|  |  |  |                 value = func(value, operation["value"]) | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |             ctx.stored_data[args["key"]] = args["value"] = value | 
					
						
							|  |  |  |             targets = set(ctx.stored_data_notification_clients[args["key"]]) | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |             if args.get("want_reply", True): | 
					
						
							|  |  |  |                 targets.add(client) | 
					
						
							|  |  |  |             if targets: | 
					
						
							|  |  |  |                 ctx.broadcast(targets, [args]) | 
					
						
							| 
									
										
										
										
											2023-04-08 08:20:59 +02:00
										 |  |  |             ctx.save() | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |         elif cmd == "SetNotify": | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |             if "keys" not in args or type(args["keys"]) != list: | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", | 
					
						
							| 
									
										
										
										
											2022-02-22 12:17:21 +01:00
										 |  |  |                                               "text": 'SetNotify', "original_cmd": cmd}]) | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2022-03-04 21:36:18 +01:00
										 |  |  |             for key in args["keys"]: | 
					
						
							| 
									
										
										
										
											2022-02-22 11:48:08 +01:00
										 |  |  |                 ctx.stored_data_notification_clients[key].add(client) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | def update_client_status(ctx: Context, client: Client, new_status: ClientStatus): | 
					
						
							| 
									
										
										
										
											2021-03-07 22:05:07 +01:00
										 |  |  |     current = ctx.client_game_state[client.team, client.slot] | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     if current != ClientStatus.CLIENT_GOAL:  # can't undo goal completion | 
					
						
							|  |  |  |         if new_status == ClientStatus.CLIENT_GOAL: | 
					
						
							| 
									
										
										
										
											2021-11-11 16:09:08 +01:00
										 |  |  |             ctx.on_goal_achieved(client) | 
					
						
							| 
									
										
										
										
											2024-04-14 13:16:45 -05:00
										 |  |  |             # if player has yet to ever connect to the server, they will not be in client_game_state | 
					
						
							|  |  |  |             if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL | 
					
						
							|  |  |  |                    for player in ctx.player_names | 
					
						
							|  |  |  |                    if player[0] == client.team and player[1] != client.slot): | 
					
						
							|  |  |  |                 ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!") | 
					
						
							| 
									
										
										
										
											2021-03-07 22:05:07 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         ctx.client_game_state[client.team, client.slot] = new_status | 
					
						
							| 
									
										
										
										
											2023-11-24 17:14:07 -06:00
										 |  |  |         ctx.on_client_status_change(client.team, client.slot) | 
					
						
							| 
									
										
										
										
											2023-04-08 08:20:59 +02:00
										 |  |  |         ctx.save() | 
					
						
							| 
									
										
										
										
											2020-02-17 13:57:48 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-05 21:45:52 +02:00
										 |  |  | class ServerCommandProcessor(CommonCommandProcessor): | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |     def __init__(self, ctx: Context): | 
					
						
							|  |  |  |         self.ctx = ctx | 
					
						
							|  |  |  |         super(ServerCommandProcessor, self).__init__() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     def output(self, text: str): | 
					
						
							|  |  |  |         if self.client: | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |             self.ctx.notify_client(self.client, text, {"type": "AdminCommandResult"}) | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |         super(ServerCommandProcessor, self).output(text) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |     def default(self, raw: str): | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |         self.ctx.broadcast_text_all('[Server]: ' + raw, {"type": "ServerChat", "message": raw}) | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  |     def _cmd_save(self) -> bool: | 
					
						
							|  |  |  |         """Save current state to multidata""" | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |         if self.ctx.saving: | 
					
						
							|  |  |  |             self.ctx.save(True) | 
					
						
							|  |  |  |             self.output("Game saved") | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.output("Saving is disabled.") | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-23 06:16:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |     def _cmd_players(self) -> bool: | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         """Get information about connected players""" | 
					
						
							| 
									
										
										
										
											2020-04-19 14:51:48 +02:00
										 |  |  |         self.output(get_players_string(self.ctx)) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-03 14:03:49 +02:00
										 |  |  |     def _cmd_status(self, tag: str = "") -> bool: | 
					
						
							|  |  |  |         """Get status information about teams.
 | 
					
						
							|  |  |  |         Optionally mention a Tag name and get information on who has that Tag. | 
					
						
							|  |  |  |         For example: DeathLink or EnergyLink."""
 | 
					
						
							|  |  |  |         for team in self.ctx.clients: | 
					
						
							|  |  |  |             self.output(get_status_string(self.ctx, team, tag)) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |     def _cmd_exit(self) -> bool: | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         """Shutdown the server""" | 
					
						
							| 
									
										
										
										
											2023-04-23 22:21:28 +02:00
										 |  |  |         self.ctx.server.ws_server.close() | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |         self.ctx.exit_event.set() | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-27 03:47:12 +02:00
										 |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_alias(self, player_name_then_alias_name): | 
					
						
							| 
									
										
										
										
											2020-06-10 06:13:14 +02:00
										 |  |  |         """Set a player's alias, by listing their base name and then their intended alias.""" | 
					
						
							| 
									
										
										
										
											2024-04-21 11:37:24 -05:00
										 |  |  |         player_name, _, alias_name = player_name_then_alias_name.partition(" ") | 
					
						
							| 
									
										
										
										
											2020-04-27 03:47:12 +02:00
										 |  |  |         player_name, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | 
					
						
							|  |  |  |         if usable: | 
					
						
							|  |  |  |             for (team, slot), name in self.ctx.player_names.items(): | 
					
						
							|  |  |  |                 if name == player_name: | 
					
						
							|  |  |  |                     if alias_name: | 
					
						
							|  |  |  |                         alias_name = alias_name.strip()[:15] | 
					
						
							|  |  |  |                         self.ctx.name_aliases[team, slot] = alias_name | 
					
						
							|  |  |  |                         self.output(f"Named {player_name} as {alias_name}") | 
					
						
							|  |  |  |                         update_aliases(self.ctx, team) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |                         self.ctx.save() | 
					
						
							| 
									
										
										
										
											2020-04-27 03:47:12 +02:00
										 |  |  |                         return True | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         del (self.ctx.name_aliases[team, slot]) | 
					
						
							|  |  |  |                         self.output(f"Removed Alias for {player_name}") | 
					
						
							|  |  |  |                         update_aliases(self.ctx, team) | 
					
						
							| 
									
										
										
										
											2020-06-20 15:46:33 +02:00
										 |  |  |                         self.ctx.save() | 
					
						
							| 
									
										
										
										
											2020-04-27 03:47:12 +02:00
										 |  |  |                         return True | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.output(response) | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |     def resolve_player(self, input_name: str) -> typing.Optional[typing.Tuple[int, int, str]]: | 
					
						
							|  |  |  |         """ returns (team, slot, player name) """ | 
					
						
							| 
									
										
										
										
											2022-10-30 15:49:12 +01:00
										 |  |  |         # TODO: clean up once we disallow multidata < 0.3.6, which has CI unique names | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |         # first match case | 
					
						
							|  |  |  |         for (team, slot), name in self.ctx.player_names.items(): | 
					
						
							|  |  |  |             if name == input_name: | 
					
						
							|  |  |  |                 return team, slot, name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # if no case-sensitive match, then match without case only if there's only 1 match | 
					
						
							|  |  |  |         input_lower = input_name.lower() | 
					
						
							|  |  |  |         match: typing.Optional[typing.Tuple[int, int, str]] = None | 
					
						
							|  |  |  |         for (team, slot), name in self.ctx.player_names.items(): | 
					
						
							|  |  |  |             lowered = name.lower() | 
					
						
							|  |  |  |             if lowered == input_lower: | 
					
						
							|  |  |  |                 if match: | 
					
						
							|  |  |  |                     return None  # ambiguous input_name | 
					
						
							|  |  |  |                 match = (team, slot, name) | 
					
						
							|  |  |  |         return match | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_collect(self, player_name: str) -> bool: | 
					
						
							|  |  |  |         """Send out the remaining items to player.""" | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |         player = self.resolve_player(player_name) | 
					
						
							|  |  |  |         if player: | 
					
						
							|  |  |  |             team, slot, _ = player | 
					
						
							|  |  |  |             collect_player(self.ctx, team, slot) | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         self.output(f"Could not find player {player_name} to collect") | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |     @mark_raw | 
					
						
							|  |  |  |     def _cmd_release(self, player_name: str) -> bool: | 
					
						
							|  |  |  |         """Send out the remaining items from a player to their intended recipients.""" | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |         player = self.resolve_player(player_name) | 
					
						
							|  |  |  |         if player: | 
					
						
							|  |  |  |             team, slot, _ = player | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |             release_player(self.ctx, team, slot) | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |             return True | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |         self.output(f"Could not find player {player_name} to release") | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |     def _cmd_allow_release(self, player_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |         """Allow the specified player to use the !release command.""" | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |         player = self.resolve_player(player_name) | 
					
						
							|  |  |  |         if player: | 
					
						
							|  |  |  |             team, slot, name = player | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |             self.ctx.allow_releases[(team, slot)] = True | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |             self.output(f"Player {name} is now allowed to use the !release command at any time.") | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |         self.output(f"Could not find player {player_name} to allow the !release command for.") | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @mark_raw | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |     def _cmd_forbid_release(self, player_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2024-03-09 23:12:55 -08:00
										 |  |  |         """Disallow the specified player from using the !release command.""" | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |         player = self.resolve_player(player_name) | 
					
						
							|  |  |  |         if player: | 
					
						
							|  |  |  |             team, slot, name = player | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |             self.ctx.allow_releases[(team, slot)] = False | 
					
						
							| 
									
										
										
										
											2022-10-29 15:41:07 -07:00
										 |  |  |             self.output(f"Player {name} has to follow the server restrictions on use of the !release command.") | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 19:59:42 -05:00
										 |  |  |         self.output(f"Could not find player {player_name} to forbid the !release command for.") | 
					
						
							| 
									
										
										
										
											2020-09-02 02:23:31 -07:00
										 |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-31 19:05:00 +01:00
										 |  |  |     def _cmd_send_multiple(self, amount: typing.Union[int, str], player_name: str, *item_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2022-01-29 15:10:02 +01:00
										 |  |  |         """Sends multiples of an item to the specified player""" | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | 
					
						
							|  |  |  |         if usable: | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |             team, slot = self.ctx.player_name_lookup[seeked_player] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             item_name = " ".join(item_name) | 
					
						
							|  |  |  |             names = self.ctx.item_names_for_game(self.ctx.games[slot]) | 
					
						
							|  |  |  |             item_name, usable, response = get_intended_text(item_name, names) | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |             if usable: | 
					
						
							| 
									
										
										
										
											2022-01-31 19:05:00 +01:00
										 |  |  |                 amount: int = int(amount) | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                 new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 send_items_to(self.ctx, team, slot, *new_items) | 
					
						
							| 
									
										
										
										
											2022-01-31 19:05:00 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 send_new_items(self.ctx) | 
					
						
							| 
									
										
										
										
											2023-02-05 22:06:38 +01:00
										 |  |  |                 self.ctx.broadcast_text_all( | 
					
						
							| 
									
										
										
										
											2022-01-31 19:05:00 +01:00
										 |  |  |                     'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                     f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') | 
					
						
							| 
									
										
										
										
											2021-07-12 14:35:44 +02:00
										 |  |  |                 return True | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output(response) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-29 15:10:02 +01:00
										 |  |  |     def _cmd_send(self, player_name: str, *item_name: str) -> bool: | 
					
						
							|  |  |  |         """Sends an item to the specified player""" | 
					
						
							| 
									
										
										
										
											2022-01-31 19:05:00 +01:00
										 |  |  |         return self._cmd_send_multiple(1, player_name, *item_name) | 
					
						
							| 
									
										
										
										
											2022-01-29 15:10:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-20 16:38:34 -05:00
										 |  |  |     def _cmd_send_location(self, player_name: str, *location_name: str) -> bool: | 
					
						
							|  |  |  |         """Send out item from a player's location as though they checked it""" | 
					
						
							|  |  |  |         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | 
					
						
							|  |  |  |         if usable: | 
					
						
							|  |  |  |             team, slot = self.ctx.player_name_lookup[seeked_player] | 
					
						
							|  |  |  |             game = self.ctx.games[slot] | 
					
						
							|  |  |  |             full_name = " ".join(location_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if full_name.isnumeric(): | 
					
						
							|  |  |  |                 location, usable, response = int(full_name), True, None | 
					
						
							|  |  |  |             elif self.ctx.location_names_for_game(game) is not None: | 
					
						
							|  |  |  |                 location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game)) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output("Can't look up location for unknown game. Send by ID instead.") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if usable: | 
					
						
							|  |  |  |                 if isinstance(location, int): | 
					
						
							|  |  |  |                     register_location_checks(self.ctx, team, slot, [location]) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     seeked_location: int = self.ctx.location_names_for_game(self.ctx.games[slot])[location] | 
					
						
							|  |  |  |                     register_location_checks(self.ctx, team, slot, [seeked_location]) | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.output(response) | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _cmd_hint(self, player_name: str, *item_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |         """Send out a hint for a player's item to their team""" | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | 
					
						
							|  |  |  |         if usable: | 
					
						
							| 
									
										
										
										
											2021-07-12 15:33:20 +02:00
										 |  |  |             team, slot = self.ctx.player_name_lookup[seeked_player] | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |             game = self.ctx.games[slot] | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             full_name = " ".join(item_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if full_name.isnumeric(): | 
					
						
							|  |  |  |                 item, usable, response = int(full_name), True, None | 
					
						
							|  |  |  |             elif game in self.ctx.all_item_and_group_names: | 
					
						
							|  |  |  |                 item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game]) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output("Can't look up item for unknown game. Hint for ID instead.") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-12 15:33:20 +02:00
										 |  |  |             if usable: | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]: | 
					
						
							| 
									
										
										
										
											2021-07-12 15:33:20 +02:00
										 |  |  |                     hints = [] | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                     for item_name_from_group in self.ctx.item_name_groups[game][item]: | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |                         if item_name_from_group in self.ctx.item_names_for_game(game):  # ensure item has an ID | 
					
						
							|  |  |  |                             hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 else:  # item name or id | 
					
						
							|  |  |  |                     hints = collect_hints(self.ctx, team, slot, item) | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |                 if hints: | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |                     self.ctx.notify_hints(team, hints) | 
					
						
							| 
									
										
										
										
											2022-01-30 14:14:49 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     self.output("No hints found.") | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.output(response) | 
					
						
							|  |  |  |             return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-07 18:28:50 +02:00
										 |  |  |     def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |         """Send out a hint for a player's location to their team""" | 
					
						
							|  |  |  |         seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) | 
					
						
							|  |  |  |         if usable: | 
					
						
							|  |  |  |             team, slot = self.ctx.player_name_lookup[seeked_player] | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             game = self.ctx.games[slot] | 
					
						
							|  |  |  |             full_name = " ".join(location_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if full_name.isnumeric(): | 
					
						
							|  |  |  |                 location, usable, response = int(full_name), True, None | 
					
						
							| 
									
										
										
										
											2024-04-13 19:25:27 -05:00
										 |  |  |             elif game in self.ctx.all_location_and_group_names: | 
					
						
							|  |  |  |                 location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game]) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 self.output("Can't look up location for unknown game. Hint for ID instead.") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-16 02:20:37 +01:00
										 |  |  |             if usable: | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 if isinstance(location, int): | 
					
						
							|  |  |  |                     hints = collect_hint_location_id(self.ctx, team, slot, location) | 
					
						
							| 
									
										
										
										
											2024-04-13 19:25:27 -05:00
										 |  |  |                 elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]: | 
					
						
							|  |  |  |                     hints = [] | 
					
						
							|  |  |  |                     for loc_name_from_group in self.ctx.location_name_groups[game][location]: | 
					
						
							|  |  |  |                         if loc_name_from_group in self.ctx.location_names_for_game(game): | 
					
						
							|  |  |  |                             hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group)) | 
					
						
							| 
									
										
										
										
											2022-09-20 01:31:08 +02:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     hints = collect_hint_location_name(self.ctx, team, slot, location) | 
					
						
							| 
									
										
										
										
											2021-07-12 15:33:20 +02:00
										 |  |  |                 if hints: | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |                     self.ctx.notify_hints(team, hints) | 
					
						
							| 
									
										
										
										
											2021-07-12 15:33:20 +02:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     self.output("No hints found.") | 
					
						
							|  |  |  |                 return True | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.output(response) | 
					
						
							|  |  |  |                 return False | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             self.output(response) | 
					
						
							| 
									
										
										
										
											2020-04-21 06:26:51 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-18 11:46:48 -05:00
										 |  |  |     def _cmd_option(self, option_name: str, option_value: str): | 
					
						
							|  |  |  |         """Set an option for the server.""" | 
					
						
							|  |  |  |         value_type = self.ctx.simple_options.get(option_name, None) | 
					
						
							|  |  |  |         if not value_type: | 
					
						
							|  |  |  |             known_options = (f"{option}: {option_type}" for option, option_type in self.ctx.simple_options.items()) | 
					
						
							|  |  |  |             self.output(f"Unrecognized option '{option_name}', known: {', '.join(known_options)}") | 
					
						
							| 
									
										
										
										
											2020-06-27 14:16:51 +02:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2020-04-13 11:26:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-18 11:46:48 -05:00
										 |  |  |         if value_type == bool: | 
					
						
							|  |  |  |             def value_type(input_text: str): | 
					
						
							|  |  |  |                 return input_text.lower() not in {"off", "0", "false", "none", "null", "no"} | 
					
						
							|  |  |  |         elif value_type == str and option_name.endswith("password"): | 
					
						
							|  |  |  |             def value_type(input_text: str): | 
					
						
							|  |  |  |                 return None if input_text.lower() in {"null", "none", '""', "''"} else input_text | 
					
						
							|  |  |  |         elif value_type == str and option_name.endswith("mode"): | 
					
						
							|  |  |  |             valid_values = {"goal", "enabled", "disabled"} | 
					
						
							|  |  |  |             valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else []) | 
					
						
							|  |  |  |             if option_value.lower() not in valid_values: | 
					
						
							|  |  |  |                 self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}") | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         setattr(self.ctx, option_name, value_type(option_value)) | 
					
						
							|  |  |  |         self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}") | 
					
						
							|  |  |  |         if option_name in {"release_mode", "remaining_mode", "collect_mode"}: | 
					
						
							|  |  |  |             self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}]) | 
					
						
							|  |  |  |         elif option_name in {"hint_cost", "location_check_points"}: | 
					
						
							|  |  |  |             self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}]) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-21 03:47:01 +02:00
										 |  |  |     def _cmd_datastore(self): | 
					
						
							|  |  |  |         """Debug Tool: list writable datastorage keys and approximate the size of their values with pickle.""" | 
					
						
							|  |  |  |         total: int = 0 | 
					
						
							|  |  |  |         texts = [] | 
					
						
							|  |  |  |         for key, value in self.ctx.stored_data.items(): | 
					
						
							|  |  |  |             size = len(pickle.dumps(value)) | 
					
						
							|  |  |  |             total += size | 
					
						
							|  |  |  |             texts.append(f"Key: {key} | Size: {size}B") | 
					
						
							|  |  |  |         texts.insert(0, f"Found {len(self.ctx.stored_data)} keys, " | 
					
						
							|  |  |  |                         f"approximately totaling {Utils.format_SI_prefix(total, power=1024)}B") | 
					
						
							|  |  |  |         self.output("\n".join(texts)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 03:03:06 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-07 17:24:51 +01:00
										 |  |  | async def console(ctx: Context): | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |     import sys | 
					
						
							|  |  |  |     queue = asyncio.Queue() | 
					
						
							| 
									
										
										
										
											2023-08-04 10:01:51 +02:00
										 |  |  |     worker = Utils.stream_input(sys.stdin, queue) | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |     while not ctx.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2020-01-15 00:34:12 +01:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |             # I don't get why this while loop is needed. Works fine without it on clients, | 
					
						
							|  |  |  |             # but the queue.get() for server never fulfills if the queue is empty when entering the await. | 
					
						
							|  |  |  |             while queue.qsize() == 0: | 
					
						
							|  |  |  |                 await asyncio.sleep(0.05) | 
					
						
							| 
									
										
										
										
											2023-08-04 10:01:51 +02:00
										 |  |  |                 if not worker.is_alive(): | 
					
						
							|  |  |  |                     return | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |             input_text = await queue.get() | 
					
						
							|  |  |  |             queue.task_done() | 
					
						
							| 
									
										
										
										
											2020-04-14 20:22:42 +02:00
										 |  |  |             ctx.commandprocessor(input_text) | 
					
						
							| 
									
										
										
										
											2020-01-15 00:34:12 +01:00
										 |  |  |         except: | 
					
						
							|  |  |  |             import traceback | 
					
						
							|  |  |  |             traceback.print_exc() | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-22 21:23:39 +01:00
										 |  |  | def parse_args() -> argparse.Namespace: | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     parser = argparse.ArgumentParser() | 
					
						
							| 
									
										
										
										
											2024-04-24 06:24:44 +02:00
										 |  |  |     defaults = Utils.get_settings()["server_options"].as_dict() | 
					
						
							| 
									
										
										
										
											2021-07-23 02:19:41 +02:00
										 |  |  |     parser.add_argument('multidata', nargs="?", default=defaults["multidata"]) | 
					
						
							| 
									
										
										
										
											2020-04-02 11:21:33 +02:00
										 |  |  |     parser.add_argument('--host', default=defaults["host"]) | 
					
						
							|  |  |  |     parser.add_argument('--port', default=defaults["port"], type=int) | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     parser.add_argument('--server_password', default=defaults["server_password"]) | 
					
						
							| 
									
										
										
										
											2020-04-02 11:21:33 +02:00
										 |  |  |     parser.add_argument('--password', default=defaults["password"]) | 
					
						
							|  |  |  |     parser.add_argument('--savefile', default=defaults["savefile"]) | 
					
						
							|  |  |  |     parser.add_argument('--disable_save', default=defaults["disable_save"], action='store_true') | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  |     parser.add_argument('--cert', help="Path to a SSL Certificate for encryption.") | 
					
						
							|  |  |  |     parser.add_argument('--cert_key', help="Path to SSL Certificate Key file") | 
					
						
							| 
									
										
										
										
											2020-04-02 11:21:33 +02:00
										 |  |  |     parser.add_argument('--loglevel', default=defaults["loglevel"], | 
					
						
							|  |  |  |                         choices=['debug', 'info', 'warning', 'error', 'critical']) | 
					
						
							|  |  |  |     parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) | 
					
						
							|  |  |  |     parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) | 
					
						
							|  |  |  |     parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |     parser.add_argument('--release_mode', default=defaults["release_mode"], nargs='?', | 
					
						
							| 
									
										
										
										
											2020-05-14 15:17:56 -07:00
										 |  |  |                         choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
 | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                              Select !release Accessibility. (default: %(default)s) | 
					
						
							|  |  |  |                              auto:     Automatic "release" on goal completion | 
					
						
							|  |  |  |                              enabled:  !release is always available | 
					
						
							|  |  |  |                              disabled: !release is never available | 
					
						
							|  |  |  |                              goal:     !release can be used after goal completion | 
					
						
							|  |  |  |                              auto-enabled: !release is available and automatically triggered on goal completion | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |                              ''')
 | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |     parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?', | 
					
						
							|  |  |  |                         choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
 | 
					
						
							|  |  |  |                              Select !collect Accessibility. (default: %(default)s) | 
					
						
							|  |  |  |                              auto:     Automatic "collect" on goal completion | 
					
						
							|  |  |  |                              enabled:  !collect is always available | 
					
						
							|  |  |  |                              disabled: !collect is never available | 
					
						
							|  |  |  |                              goal:     !collect can be used after goal completion | 
					
						
							|  |  |  |                              auto-enabled: !collect is available and automatically triggered on goal completion | 
					
						
							|  |  |  |                              ''')
 | 
					
						
							| 
									
										
										
										
											2020-04-25 15:11:58 +02:00
										 |  |  |     parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?', | 
					
						
							|  |  |  |                         choices=['enabled', 'disabled', "goal"], help='''\
 | 
					
						
							|  |  |  |                              Select !remaining Accessibility. (default: %(default)s) | 
					
						
							|  |  |  |                              enabled:  !remaining is always available | 
					
						
							|  |  |  |                              disabled: !remaining is never available | 
					
						
							|  |  |  |                              goal:     !remaining can be used after goal completion | 
					
						
							|  |  |  |                              ''')
 | 
					
						
							| 
									
										
										
										
											2020-06-13 22:49:57 +02:00
										 |  |  |     parser.add_argument('--auto_shutdown', default=defaults["auto_shutdown"], type=int, | 
					
						
							|  |  |  |                         help="automatically shut down the server after this many minutes without new location checks. " | 
					
						
							|  |  |  |                              "0 to keep running. Not yet implemented.") | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |     parser.add_argument('--use_embedded_options', action="store_true", | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                         help='retrieve release, remaining and hint options from the multidata file,' | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |                              ' instead of host.yaml') | 
					
						
							| 
									
										
										
										
											2020-07-16 16:57:38 +02:00
										 |  |  |     parser.add_argument('--compatibility', default=defaults["compatibility"], type=int, | 
					
						
							|  |  |  |                         help="""
 | 
					
						
							|  |  |  |     #2 -> recommended for casual/cooperative play, attempt to be compatible with everything across all versions | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |     #1 -> recommended for friendly racing, tries to block third party clients | 
					
						
							| 
									
										
										
										
											2020-07-16 16:57:38 +02:00
										 |  |  |     #0 -> recommended for tournaments to force a level playing field, only allow an exact version match | 
					
						
							|  |  |  |     """)
 | 
					
						
							| 
									
										
										
										
											2021-04-07 02:37:21 +02:00
										 |  |  |     parser.add_argument('--log_network', default=defaults["log_network"], action="store_true") | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     args = parser.parse_args() | 
					
						
							| 
									
										
										
										
											2020-03-22 21:23:39 +01:00
										 |  |  |     return args | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | async def auto_shutdown(ctx, to_cancel=None): | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |     with contextlib.suppress(asyncio.TimeoutError): | 
					
						
							|  |  |  |         await asyncio.wait_for(ctx.exit_event.wait(), ctx.auto_shutdown) | 
					
						
							| 
									
										
										
										
											2024-01-07 01:42:57 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def inactivity_shutdown(): | 
					
						
							|  |  |  |         ctx.server.ws_server.close() | 
					
						
							|  |  |  |         ctx.exit_event.set() | 
					
						
							|  |  |  |         if to_cancel: | 
					
						
							|  |  |  |             for task in to_cancel: | 
					
						
							|  |  |  |                 task.cancel() | 
					
						
							| 
									
										
										
										
											2024-05-17 12:21:01 +02:00
										 |  |  |         ctx.logger.info("Shutting down due to inactivity.") | 
					
						
							| 
									
										
										
										
											2024-01-07 01:42:57 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |     while not ctx.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |         if not ctx.client_activity_timers.values(): | 
					
						
							| 
									
										
										
										
											2024-01-07 01:42:57 +01:00
										 |  |  |             inactivity_shutdown() | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             newest_activity = max(ctx.client_activity_timers.values()) | 
					
						
							|  |  |  |             delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity | 
					
						
							| 
									
										
										
										
											2020-07-10 17:42:22 +02:00
										 |  |  |             seconds = ctx.auto_shutdown - delta.total_seconds() | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |             if seconds < 0: | 
					
						
							| 
									
										
										
										
											2024-01-07 01:42:57 +01:00
										 |  |  |                 inactivity_shutdown() | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |             else: | 
					
						
							| 
									
										
										
										
											2024-06-06 01:54:46 +02:00
										 |  |  |                 with contextlib.suppress(asyncio.TimeoutError): | 
					
						
							|  |  |  |                     await asyncio.wait_for(ctx.exit_event.wait(), seconds) | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-13 22:49:57 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  | def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLContext": | 
					
						
							|  |  |  |     import ssl | 
					
						
							|  |  |  |     ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | 
					
						
							|  |  |  |     ssl_context.load_default_certs() | 
					
						
							|  |  |  |     ssl_context.load_cert_chain(path, cert_key if cert_key else path) | 
					
						
							|  |  |  |     return ssl_context | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-22 21:23:39 +01:00
										 |  |  | async def main(args: argparse.Namespace): | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  |     Utils.init_logging("Server", loglevel=args.loglevel.lower()) | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-21 22:11:19 -07:00
										 |  |  |     ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, | 
					
						
							| 
									
										
										
										
											2023-01-02 12:29:21 -06:00
										 |  |  |                   args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |                   args.remaining_mode, | 
					
						
							| 
									
										
										
										
											2021-08-26 16:19:37 +02:00
										 |  |  |                   args.auto_shutdown, args.compatibility, args.log_network) | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |     data_filename = args.multidata | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 10:04:17 +02:00
										 |  |  |     if not data_filename: | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2022-06-04 18:36:50 +02:00
										 |  |  |             filetypes = (("Multiworld data", (".archipelago", ".zip")),) | 
					
						
							|  |  |  |             data_filename = Utils.open_filename("Select multiworld data", filetypes) | 
					
						
							| 
									
										
										
										
											2021-07-23 02:19:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 10:04:17 +02:00
										 |  |  |         except Exception as e: | 
					
						
							|  |  |  |             if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)): | 
					
						
							|  |  |  |                 if not isinstance(e, ImportError): | 
					
						
							|  |  |  |                     logging.error(f"Failed to load tkinter ({e})") | 
					
						
							|  |  |  |                 logging.info("Pass a multidata filename on command line to run headless.") | 
					
						
							| 
									
										
										
										
											2023-06-28 07:56:00 +02:00
										 |  |  |                 # when cx_Freeze'd the built-in exit is not available, so we import sys.exit instead | 
					
						
							|  |  |  |                 import sys | 
					
						
							|  |  |  |                 sys.exit(1) | 
					
						
							| 
									
										
										
										
											2022-09-16 10:04:17 +02:00
										 |  |  |             raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not data_filename: | 
					
						
							|  |  |  |             logging.info("No file selected. Exiting.") | 
					
						
							| 
									
										
										
										
											2023-06-28 07:56:00 +02:00
										 |  |  |             import sys | 
					
						
							|  |  |  |             sys.exit(1) | 
					
						
							| 
									
										
										
										
											2022-09-16 10:04:17 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2020-06-13 08:37:05 +02:00
										 |  |  |         ctx.load(data_filename, args.use_embedded_options) | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |     except Exception as e: | 
					
						
							| 
									
										
										
										
											2022-09-16 10:04:17 +02:00
										 |  |  |         logging.exception(f"Failed to read multiworld data ({e})") | 
					
						
							| 
									
										
										
										
											2020-06-07 00:50:39 +02:00
										 |  |  |         raise | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |     ctx.init_save(not args.disable_save) | 
					
						
							| 
									
										
										
										
											2020-05-30 03:47:40 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-21 17:29:27 +01:00
										 |  |  |     ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-15 17:10:33 +02:00
										 |  |  |     ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context) | 
					
						
							| 
									
										
										
										
											2020-06-07 00:49:10 +02:00
										 |  |  |     ip = args.host if args.host else Utils.get_public_ipv4() | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  |     logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, | 
					
						
							|  |  |  |                                                  'No password' if not ctx.password else 'Password: %s' % ctx.password)) | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  |     await ctx.server | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |     console_task = asyncio.create_task(console(ctx)) | 
					
						
							|  |  |  |     if ctx.auto_shutdown: | 
					
						
							|  |  |  |         ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task])) | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |     await ctx.exit_event.wait() | 
					
						
							|  |  |  |     console_task.cancel() | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |     if ctx.shutdown_task: | 
					
						
							|  |  |  |         await ctx.shutdown_task | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-27 13:52:03 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | client_message_processor = ClientMessageProcessor | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-09 19:27:56 +01:00
										 |  |  | if __name__ == '__main__': | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |     try: | 
					
						
							| 
									
										
										
										
											2020-06-21 15:32:31 +02:00
										 |  |  |         asyncio.run(main(parse_args())) | 
					
						
							| 
									
										
										
										
											2020-06-16 11:26:54 +02:00
										 |  |  |     except asyncio.exceptions.CancelledError: | 
					
						
							|  |  |  |         pass |