| 
									
										
											  
											
												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
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +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
										 |  |  | import typing | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  | import enum | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  | import warnings | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | from json import JSONEncoder, JSONDecoder | 
					
						
							| 
									
										
											  
											
												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
										 |  |  | 
 | 
					
						
							|  |  |  | import websockets | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-02 08:23:39 +02:00
										 |  |  | from Utils import ByValue, Version | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | class JSONMessagePart(typing.TypedDict, total=False): | 
					
						
							|  |  |  |     text: str | 
					
						
							|  |  |  |     # optional | 
					
						
							|  |  |  |     type: str | 
					
						
							|  |  |  |     color: str | 
					
						
							| 
									
										
										
										
											2021-11-07 14:42:05 +01:00
										 |  |  |     # owning player for location/item | 
					
						
							|  |  |  |     player: int | 
					
						
							| 
									
										
										
										
											2022-01-18 06:43:08 +01:00
										 |  |  |     # if type == item indicates item flags | 
					
						
							|  |  |  |     flags: int | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-02 08:23:39 +02:00
										 |  |  | class ClientStatus(ByValue, enum.IntEnum): | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  |     CLIENT_UNKNOWN = 0 | 
					
						
							| 
									
										
										
										
											2021-03-07 22:05:07 +01:00
										 |  |  |     CLIENT_CONNECTED = 5 | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  |     CLIENT_READY = 10 | 
					
						
							|  |  |  |     CLIENT_PLAYING = 20 | 
					
						
							|  |  |  |     CLIENT_GOAL = 30 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-02 08:23:39 +02:00
										 |  |  | class SlotType(ByValue, enum.IntFlag): | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |     spectator = 0b00 | 
					
						
							|  |  |  |     player = 0b01 | 
					
						
							|  |  |  |     group = 0b10 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def always_goal(self) -> bool: | 
					
						
							| 
									
										
										
										
											2023-03-21 15:53:10 +01:00
										 |  |  |         """Mark this slot as having reached its goal instantly.""" | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |         return self.value != 0b01 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-02 08:23:39 +02:00
										 |  |  | class Permission(ByValue, enum.IntFlag): | 
					
						
							| 
									
										
										
										
											2021-09-26 09:06:12 +02:00
										 |  |  |     disabled = 0b000  # 0, completely disables access | 
					
						
							|  |  |  |     enabled = 0b001  # 1, allows manual use | 
					
						
							|  |  |  |     goal = 0b010  # 2, allows manual use after goal completion | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |     auto = 0b110  # 6, forces use after goal completion, only works for release | 
					
						
							| 
									
										
										
										
											2021-09-26 09:06:12 +02:00
										 |  |  |     auto_enabled = 0b111  # 7, forces use after goal completion, allows manual use any time | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def from_text(text: str): | 
					
						
							|  |  |  |         data = 0 | 
					
						
							|  |  |  |         if "auto" in text: | 
					
						
							|  |  |  |             data |= 0b110 | 
					
						
							|  |  |  |         elif "goal" in text: | 
					
						
							|  |  |  |             data |= 0b010 | 
					
						
							|  |  |  |         if "enabled" in text: | 
					
						
							|  |  |  |             data |= 0b001 | 
					
						
							|  |  |  |         return Permission(data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  | class NetworkPlayer(typing.NamedTuple): | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  |     """Represents a particular player on a particular team.""" | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  |     team: int | 
					
						
							|  |  |  |     slot: int | 
					
						
							|  |  |  |     alias: str | 
					
						
							|  |  |  |     name: str | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | class NetworkSlot(typing.NamedTuple): | 
					
						
							|  |  |  |     """Represents a particular slot across teams.""" | 
					
						
							|  |  |  |     name: str | 
					
						
							|  |  |  |     game: str | 
					
						
							|  |  |  |     type: SlotType | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |     group_members: typing.Union[typing.List[int], typing.Tuple] = ()  # only populated if type == group | 
					
						
							| 
									
										
										
										
											2022-01-30 13:57:12 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  | class NetworkItem(typing.NamedTuple): | 
					
						
							|  |  |  |     item: int | 
					
						
							|  |  |  |     location: int | 
					
						
							|  |  |  |     player: int | 
					
						
							| 
									
										
										
										
											2024-08-19 11:37:36 -07:00
										 |  |  |     """ Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """ | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |     flags: int = 0 | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any: | 
					
						
							|  |  |  |     if isinstance(obj, tuple) and hasattr(obj, "_fields"):  # NamedTuple is not actually a parent class | 
					
						
							|  |  |  |         data = obj._asdict() | 
					
						
							|  |  |  |         data["class"] = obj.__class__.__name__ | 
					
						
							|  |  |  |         return data | 
					
						
							| 
									
										
										
										
											2022-12-31 19:52:04 +01:00
										 |  |  |     if isinstance(obj, (tuple, list, set, frozenset)): | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |         return tuple(_scan_for_TypedTuples(o) for o in obj) | 
					
						
							|  |  |  |     if isinstance(obj, dict): | 
					
						
							|  |  |  |         return {key: _scan_for_TypedTuples(value) for key, value in obj.items()} | 
					
						
							|  |  |  |     return obj | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | _encode = JSONEncoder( | 
					
						
							|  |  |  |     ensure_ascii=False, | 
					
						
							|  |  |  |     check_circular=False, | 
					
						
							| 
									
										
										
										
											2022-06-20 03:00:53 +02:00
										 |  |  |     separators=(',', ':'), | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | ).encode | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  | def encode(obj: typing.Any) -> str: | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  |     return _encode(_scan_for_TypedTuples(obj)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-19 04:14:59 +01:00
										 |  |  | def get_any_version(data: dict) -> Version: | 
					
						
							|  |  |  |     data = {key.lower(): value for key, value in data.items()}  # .NET version classes have capitalized keys | 
					
						
							|  |  |  |     return Version(int(data["major"]), int(data["minor"]), int(data["build"])) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-31 19:52:04 +01:00
										 |  |  | allowlist = { | 
					
						
							| 
									
										
										
										
											2022-03-04 22:48:27 +01:00
										 |  |  |     "NetworkPlayer": NetworkPlayer, | 
					
						
							|  |  |  |     "NetworkItem": NetworkItem, | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |     "NetworkSlot": NetworkSlot | 
					
						
							| 
									
										
										
										
											2022-03-04 22:48:27 +01:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2021-03-19 04:14:59 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | custom_hooks = { | 
					
						
							|  |  |  |     "Version": get_any_version | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | def _object_hook(o: typing.Any) -> typing.Any: | 
					
						
							|  |  |  |     if isinstance(o, dict): | 
					
						
							| 
									
										
										
										
											2021-03-19 04:14:59 +01:00
										 |  |  |         hook = custom_hooks.get(o.get("class", None), None) | 
					
						
							|  |  |  |         if hook: | 
					
						
							|  |  |  |             return hook(o) | 
					
						
							| 
									
										
										
										
											2022-12-31 19:52:04 +01:00
										 |  |  |         cls = allowlist.get(o.get("class", None), None) | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  |         if cls: | 
					
						
							|  |  |  |             for key in tuple(o): | 
					
						
							|  |  |  |                 if key not in cls._fields: | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |                     del (o[key]) | 
					
						
							| 
									
										
										
										
											2021-02-28 20:32:15 +01:00
										 |  |  |             return cls(**o) | 
					
						
							| 
									
										
										
										
											2021-02-21 23:46:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return o | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | decode = JSONDecoder(object_hook=_object_hook).decode | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
											  
											
												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 Endpoint: | 
					
						
							|  |  |  |     socket: websockets.WebSocketServerProtocol | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, socket): | 
					
						
							|  |  |  |         self.socket = socket | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  | class HandlerMeta(type): | 
					
						
							|  |  |  |     def __new__(mcs, name, bases, attrs): | 
					
						
							|  |  |  |         handlers = attrs["handlers"] = {} | 
					
						
							|  |  |  |         trigger: str = "_handle_" | 
					
						
							|  |  |  |         for base in bases: | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |             handlers.update(base.handlers) | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         handlers.update({handler_name[len(trigger):]: method for handler_name, method in attrs.items() if | 
					
						
							|  |  |  |                          handler_name.startswith(trigger)}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         orig_init = attrs.get('__init__', None) | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |         if not orig_init: | 
					
						
							|  |  |  |             for base in bases: | 
					
						
							|  |  |  |                 orig_init = getattr(base, '__init__', None) | 
					
						
							|  |  |  |                 if orig_init: | 
					
						
							|  |  |  |                     break | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         def __init__(self, *args, **kwargs): | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +01:00
										 |  |  |             if orig_init: | 
					
						
							|  |  |  |                 orig_init(self, *args, **kwargs) | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |             # turn functions into bound methods | 
					
						
							|  |  |  |             self.handlers = {name: method.__get__(self, type(self)) for name, method in | 
					
						
							|  |  |  |                              handlers.items()} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         attrs['__init__'] = __init__ | 
					
						
							|  |  |  |         return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | class JSONTypes(str, enum.Enum): | 
					
						
							|  |  |  |     color = "color" | 
					
						
							|  |  |  |     text = "text" | 
					
						
							|  |  |  |     player_id = "player_id" | 
					
						
							|  |  |  |     player_name = "player_name" | 
					
						
							|  |  |  |     item_name = "item_name" | 
					
						
							|  |  |  |     item_id = "item_id" | 
					
						
							|  |  |  |     location_name = "location_name" | 
					
						
							|  |  |  |     location_id = "location_id" | 
					
						
							|  |  |  |     entrance_name = "entrance_name" | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  | class JSONtoTextParser(metaclass=HandlerMeta): | 
					
						
							| 
									
										
										
										
											2022-01-18 06:16:16 +01:00
										 |  |  |     color_codes = { | 
					
						
							|  |  |  |         # not exact color names, close enough but decent looking | 
					
						
							|  |  |  |         "black": "000000", | 
					
						
							|  |  |  |         "red": "EE0000", | 
					
						
							|  |  |  |         "green": "00FF7F", | 
					
						
							|  |  |  |         "yellow": "FAFAD2", | 
					
						
							|  |  |  |         "blue": "6495ED", | 
					
						
							|  |  |  |         "magenta": "EE00EE", | 
					
						
							|  |  |  |         "cyan": "00EEEE", | 
					
						
							|  |  |  |         "slateblue": "6D8BE8", | 
					
						
							|  |  |  |         "plum": "AF99EF", | 
					
						
							|  |  |  |         "salmon": "FA8072", | 
					
						
							| 
									
										
										
										
											2024-06-08 19:08:47 -07:00
										 |  |  |         "white": "FFFFFF", | 
					
						
							|  |  |  |         "orange": "FF7700", | 
					
						
							| 
									
										
										
										
											2022-01-18 06:16:16 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-26 21:49:23 +01:00
										 |  |  |     def __init__(self, ctx): | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         self.ctx = ctx | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def __call__(self, input_object: typing.List[JSONMessagePart]) -> str: | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         return "".join(self.handle_node(section) for section in input_object) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def handle_node(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-09-04 17:53:09 +02:00
										 |  |  |         node_type = node.get("type", None) | 
					
						
							|  |  |  |         handler = self.handlers.get(node_type, self.handlers["text"]) | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         return handler(node) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def _handle_color(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         codes = node["color"].split(";") | 
					
						
							| 
									
										
										
										
											2022-01-25 02:25:20 +01:00
										 |  |  |         buffer = "".join(color_code(code) for code in codes if code in color_codes) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return buffer + self._handle_text(node) + color_code("reset") | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def _handle_text(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         return node.get("text", "") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def _handle_player_id(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         player = int(node["text"]) | 
					
						
							|  |  |  |         node["color"] = 'magenta' if player == self.ctx.slot else 'yellow' | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         node["text"] = self.ctx.player_names[player] | 
					
						
							|  |  |  |         return self._handle_color(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # for other teams, spectators etc.? Only useful if player isn't in the clientside mapping | 
					
						
							| 
									
										
										
										
											2021-02-21 20:17:24 +01:00
										 |  |  |     def _handle_player_name(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-01-31 11:33:39 +01:00
										 |  |  |         node["color"] = 'yellow' | 
					
						
							|  |  |  |         return self._handle_color(node) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |     def _handle_item_name(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2022-01-18 06:43:08 +01:00
										 |  |  |         flags = node.get("flags", 0) | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         if flags == 0: | 
					
						
							|  |  |  |             node["color"] = 'cyan' | 
					
						
							| 
									
										
										
										
											2022-01-21 00:42:45 +01:00
										 |  |  |         elif flags & 0b001:  # advancement | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |             node["color"] = 'plum' | 
					
						
							| 
									
										
										
										
											2022-06-17 03:23:27 +02:00
										 |  |  |         elif flags & 0b010:  # useful | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |             node["color"] = 'slateblue' | 
					
						
							| 
									
										
										
										
											2022-01-21 00:42:45 +01:00
										 |  |  |         elif flags & 0b100:  # trap | 
					
						
							|  |  |  |             node["color"] = 'salmon' | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         else: | 
					
						
							|  |  |  |             node["color"] = 'cyan' | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return self._handle_color(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _handle_item_id(self, node: JSONMessagePart): | 
					
						
							|  |  |  |         item_id = int(node["text"]) | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |         node["text"] = self.ctx.item_names.lookup_in_slot(item_id, node["player"]) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return self._handle_item_name(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _handle_location_name(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +01:00
										 |  |  |         node["color"] = 'green' | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return self._handle_color(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _handle_location_id(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2024-06-01 06:07:13 -05:00
										 |  |  |         location_id = int(node["text"]) | 
					
						
							|  |  |  |         node["text"] = self.ctx.location_names.lookup_in_slot(location_id, node["player"]) | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +01:00
										 |  |  |         return self._handle_location_name(node) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _handle_entrance_name(self, node: JSONMessagePart): | 
					
						
							| 
									
										
										
										
											2021-08-07 05:40:18 +02:00
										 |  |  |         node["color"] = 'blue' | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return self._handle_color(node) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | class RawJSONtoTextParser(JSONtoTextParser): | 
					
						
							|  |  |  |     def _handle_color(self, node: JSONMessagePart): | 
					
						
							|  |  |  |         return self._handle_text(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, | 
					
						
							|  |  |  |                'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, | 
					
						
							| 
									
										
										
										
											2024-09-17 07:44:32 -05:00
										 |  |  |                'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47, | 
					
						
							|  |  |  |                'plum': 35, 'slateblue': 34, 'salmon': 31,}  # convert ui colors to terminal colors | 
					
						
							| 
									
										
										
										
											2021-01-21 23:37:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def color_code(*args): | 
					
						
							|  |  |  |     return '\033[' + ';'.join([str(color_codes[arg]) for arg in args]) + 'm' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def color(text, *args): | 
					
						
							|  |  |  |     return color_code(*args) + text + color_code('reset') | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def add_json_text(parts: list, text: typing.Any, **kwargs) -> None: | 
					
						
							|  |  |  |     parts.append({"text": str(text), **kwargs}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  | def add_json_item(parts: list, item_id: int, player: int = 0, item_flags: int = 0, **kwargs) -> None: | 
					
						
							| 
									
										
										
										
											2022-01-18 06:43:08 +01:00
										 |  |  |     parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs}) | 
					
						
							| 
									
										
										
										
											2021-11-07 14:42:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 20:40:58 +01:00
										 |  |  | def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs) -> None: | 
					
						
							|  |  |  |     parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs}) | 
					
						
							| 
									
										
										
										
											2021-11-07 14:42:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | class Hint(typing.NamedTuple): | 
					
						
							|  |  |  |     receiving_player: int | 
					
						
							|  |  |  |     finding_player: int | 
					
						
							|  |  |  |     location: int | 
					
						
							|  |  |  |     item: int | 
					
						
							|  |  |  |     found: bool | 
					
						
							|  |  |  |     entrance: str = "" | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |     item_flags: int = 0 | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def re_check(self, ctx, team) -> Hint: | 
					
						
							|  |  |  |         if self.found: | 
					
						
							|  |  |  |             return self | 
					
						
							|  |  |  |         found = self.location in ctx.location_checks[team, self.finding_player] | 
					
						
							|  |  |  |         if found: | 
					
						
							| 
									
										
										
										
											2022-01-18 06:43:08 +01:00
										 |  |  |             return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance, | 
					
						
							|  |  |  |                         self.item_flags) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         return self | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __hash__(self): | 
					
						
							|  |  |  |         return hash((self.receiving_player, self.finding_player, self.location, self.item, self.entrance)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def as_network_message(self) -> dict: | 
					
						
							|  |  |  |         parts = [] | 
					
						
							|  |  |  |         add_json_text(parts, "[Hint]: ") | 
					
						
							|  |  |  |         add_json_text(parts, self.receiving_player, type="player_id") | 
					
						
							|  |  |  |         add_json_text(parts, "'s ") | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |         add_json_item(parts, self.item, self.receiving_player, self.item_flags) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         add_json_text(parts, " is at ") | 
					
						
							| 
									
										
										
										
											2021-11-07 14:42:05 +01:00
										 |  |  |         add_json_location(parts, self.location, self.finding_player) | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         add_json_text(parts, " in ") | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |         add_json_text(parts, self.finding_player, type="player_id") | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         if self.entrance: | 
					
						
							|  |  |  |             add_json_text(parts, "'s World at ") | 
					
						
							|  |  |  |             add_json_text(parts, self.entrance, type="entrance_name") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             add_json_text(parts, "'s World") | 
					
						
							| 
									
										
										
										
											2021-11-30 06:41:50 +01:00
										 |  |  |         add_json_text(parts, ". ") | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         if self.found: | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +01:00
										 |  |  |             add_json_text(parts, "(found)", type="color", color="green") | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-11-30 06:09:40 +01:00
										 |  |  |             add_json_text(parts, "(not found)", type="color", color="red") | 
					
						
							| 
									
										
										
										
											2021-03-02 22:31:44 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-30 20:45:06 +02:00
										 |  |  |         return {"cmd": "PrintJSON", "data": parts, "type": "Hint", | 
					
						
							| 
									
										
										
										
											2021-06-30 20:57:00 +02:00
										 |  |  |                 "receiving": self.receiving_player, | 
					
						
							| 
									
										
										
										
											2022-01-18 05:52:29 +01:00
										 |  |  |                 "item": NetworkItem(self.item, self.location, self.finding_player, self.item_flags), | 
					
						
							| 
									
										
										
										
											2021-11-08 19:13:13 +01:00
										 |  |  |                 "found": self.found} | 
					
						
							| 
									
										
										
										
											2021-05-13 01:37:50 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def local(self): | 
					
						
							|  |  |  |         return self.receiving_player == self.finding_player | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]): | 
					
						
							| 
									
										
										
										
											2023-07-05 10:35:03 +02:00
										 |  |  |     def __init__(self, values: typing.MutableMapping[int, typing.Dict[int, typing.Tuple[int, int, int]]]): | 
					
						
							|  |  |  |         super().__init__(values) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not self: | 
					
						
							|  |  |  |             raise ValueError(f"Rejecting game with 0 players") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if len(self) != max(self): | 
					
						
							|  |  |  |             raise ValueError("Player IDs not continuous") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if len(self.get(0, {})): | 
					
						
							|  |  |  |             raise ValueError("Invalid player id 0 for location") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     def find_item(self, slots: typing.Set[int], seeked_item_id: int | 
					
						
							|  |  |  |                   ) -> typing.Generator[typing.Tuple[int, int, int, int, int], None, None]: | 
					
						
							|  |  |  |         for finding_player, check_data in self.items(): | 
					
						
							|  |  |  |             for location_id, (item_id, receiving_player, item_flags) in check_data.items(): | 
					
						
							|  |  |  |                 if receiving_player in slots and item_id == seeked_item_id: | 
					
						
							|  |  |  |                     yield finding_player, location_id, item_id, receiving_player, item_flags | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_for_player(self, slot: int) -> typing.Dict[int, typing.Set[int]]: | 
					
						
							|  |  |  |         import collections | 
					
						
							|  |  |  |         all_locations: typing.Dict[int, typing.Set[int]] = collections.defaultdict(set) | 
					
						
							|  |  |  |         for source_slot, location_data in self.items(): | 
					
						
							|  |  |  |             for location_id, values in location_data.items(): | 
					
						
							|  |  |  |                 if values[1] == slot: | 
					
						
							|  |  |  |                     all_locations[source_slot].add(location_id) | 
					
						
							|  |  |  |         return all_locations | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_checked(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int | 
					
						
							|  |  |  |                     ) -> typing.List[int]: | 
					
						
							|  |  |  |         checked = state[team, slot] | 
					
						
							|  |  |  |         if not checked: | 
					
						
							|  |  |  |             # This optimizes the case where everyone connects to a fresh game at the same time. | 
					
						
							|  |  |  |             return [] | 
					
						
							|  |  |  |         return [location_id for | 
					
						
							|  |  |  |                 location_id in self[slot] if | 
					
						
							|  |  |  |                 location_id in checked] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_missing(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int | 
					
						
							|  |  |  |                     ) -> typing.List[int]: | 
					
						
							|  |  |  |         checked = state[team, slot] | 
					
						
							|  |  |  |         if not checked: | 
					
						
							|  |  |  |             # This optimizes the case where everyone connects to a fresh game at the same time. | 
					
						
							| 
									
										
										
										
											2023-07-29 19:44:10 +02:00
										 |  |  |             return list(self[slot]) | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |         return [location_id for | 
					
						
							|  |  |  |                 location_id in self[slot] if | 
					
						
							|  |  |  |                 location_id not in checked] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int | 
					
						
							| 
									
										
										
										
											2024-08-16 13:20:02 -07:00
										 |  |  |                       ) -> typing.List[typing.Tuple[int, int]]: | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |         checked = state[team, slot] | 
					
						
							|  |  |  |         player_locations = self[slot] | 
					
						
							| 
									
										
										
										
											2024-08-16 13:20:02 -07:00
										 |  |  |         return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for | 
					
						
							|  |  |  |                         location_id in player_locations if | 
					
						
							|  |  |  |                         location_id not in checked]) | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if typing.TYPE_CHECKING:  # type-check with pure python implementation until we have a typing stub | 
					
						
							|  |  |  |     LocationStore = _LocationStore | 
					
						
							|  |  |  | else: | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         from _speedups import LocationStore | 
					
						
							| 
									
										
										
										
											2023-09-20 16:05:56 +02:00
										 |  |  |         import _speedups | 
					
						
							|  |  |  |         import os.path | 
					
						
							| 
									
										
										
										
											2023-09-22 23:05:04 +02:00
										 |  |  |         if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"): | 
					
						
							| 
									
										
										
										
											2023-09-20 16:05:56 +02:00
										 |  |  |             warnings.warn(f"{_speedups.__file__} outdated! " | 
					
						
							|  |  |  |                           f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!") | 
					
						
							| 
									
										
										
										
											2023-07-04 19:12:43 +02:00
										 |  |  |     except ImportError: | 
					
						
							| 
									
										
										
										
											2023-09-20 16:05:56 +02:00
										 |  |  |         try: | 
					
						
							|  |  |  |             import pyximport | 
					
						
							|  |  |  |             pyximport.install() | 
					
						
							|  |  |  |         except ImportError: | 
					
						
							|  |  |  |             pyximport = None | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             from _speedups import LocationStore | 
					
						
							|  |  |  |         except ImportError: | 
					
						
							|  |  |  |             warnings.warn("_speedups not available. Falling back to pure python LocationStore. " | 
					
						
							|  |  |  |                           "Install a matching C++ compiler for your platform to compile _speedups.") | 
					
						
							|  |  |  |             LocationStore = _LocationStore |