multiplayer 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
multiplayer/utils.py ADDED
@@ -0,0 +1,215 @@
1
+ """
2
+ This module provides utility functions for the multiplayer package.
3
+ """
4
+ import csv
5
+ import random
6
+ from importlib import resources
7
+ from pathlib import Path
8
+
9
+ # --- Built-in Categories ---
10
+ _BUILTIN_GAME_CATEGORIES = {
11
+ "cities": "data/cities.csv",
12
+ "countries": "data/countries.csv",
13
+ "rivers": "data/rivers.csv",
14
+ "seas_oceans": "data/seas_oceans.csv",
15
+ "planets_moons": "data/planets_moons.csv",
16
+ }
17
+
18
+ _BUILTIN_PLAYER_CATEGORIES = {
19
+ "roman_gods": "data/roman_gods.csv",
20
+ "greek_gods": "data/greek_gods.csv",
21
+ "egyptian_gods": "data/egyptian_gods.csv",
22
+ "european_kings": "data/european_kings.csv",
23
+ "european_queens": "data/european_queens.csv",
24
+ }
25
+
26
+ # --- Custom Categories (user-defined) ---
27
+ _CUSTOM_GAME_CATEGORIES = {}
28
+ _CUSTOM_PLAYER_CATEGORIES = {}
29
+
30
+ def register_name_category(category_name, data, category_type):
31
+ """
32
+ Registers a new custom category for name suggestions.
33
+
34
+ Args:
35
+ category_name (str): The name for the new category.
36
+ data (list or str or Path): A list of strings, or a path to a CSV/text file.
37
+ The file should have one name per line.
38
+ category_type (str): "game" or "player".
39
+ """
40
+ if category_type == "game":
41
+ _CUSTOM_GAME_CATEGORIES[category_name] = data
42
+ elif category_type == "player":
43
+ _CUSTOM_PLAYER_CATEGORIES[category_name] = data
44
+ else:
45
+ raise ValueError("category_type must be 'game' or 'player'")
46
+
47
+ def unregister_name_category(category_name):
48
+ """
49
+ Unregisters a custom category. Built-in categories cannot be removed.
50
+
51
+ Args:
52
+ category_name (str): The name of the custom category to remove.
53
+
54
+ Returns:
55
+ bool: True if the category was found and removed, False otherwise.
56
+ """
57
+ if category_name in _CUSTOM_GAME_CATEGORIES:
58
+ del _CUSTOM_GAME_CATEGORIES[category_name]
59
+ return True
60
+ if category_name in _CUSTOM_PLAYER_CATEGORIES:
61
+ del _CUSTOM_PLAYER_CATEGORIES[category_name]
62
+ return True
63
+ return False
64
+
65
+ def get_available_categories(category_type="all"):
66
+ """
67
+ Returns a list of available categories, including custom ones.
68
+
69
+ Args:
70
+ category_type (str): "all", "game", or "player".
71
+
72
+ Returns:
73
+ A list of strings representing the available categories.
74
+ """
75
+ if category_type == "game":
76
+ return list(_BUILTIN_GAME_CATEGORIES.keys()) + list(_CUSTOM_GAME_CATEGORIES.keys())
77
+ if category_type == "player":
78
+ return list(_BUILTIN_PLAYER_CATEGORIES.keys()) + list(_CUSTOM_PLAYER_CATEGORIES.keys())
79
+ return list({**_BUILTIN_GAME_CATEGORIES, **_BUILTIN_PLAYER_CATEGORIES, **_CUSTOM_GAME_CATEGORIES, **_CUSTOM_PLAYER_CATEGORIES}.keys())
80
+
81
+ def _get_names_from_source(source):
82
+ """Internal helper to load names from a list, a file path, or a package resource."""
83
+ if isinstance(source, list):
84
+ return source
85
+
86
+ # Ensure source uses correct separators for the OS
87
+ source_path = Path(source)
88
+
89
+ # 1. Try relative to this file first (most reliable in both dev and production layouts)
90
+ try:
91
+ current_file_dir = Path(__file__).parent.resolve()
92
+ path = (current_file_dir / source_path).resolve()
93
+ if path.is_file():
94
+ with open(path, 'r', encoding='utf-8') as f:
95
+ if str(source).endswith('.csv'):
96
+ reader = csv.reader(f)
97
+ try:
98
+ next(reader) # Assume header
99
+ return [row[0] for row in reader if row]
100
+ except StopIteration:
101
+ return []
102
+ else:
103
+ return [line.strip() for line in f if line.strip()]
104
+ except Exception:
105
+ pass
106
+
107
+ # 2. Try as an absolute or CWD-relative file path
108
+ try:
109
+ if source_path.is_file():
110
+ with open(source_path, 'r', encoding='utf-8') as f:
111
+ if str(source).endswith('.csv'):
112
+ reader = csv.reader(f)
113
+ try:
114
+ next(reader)
115
+ return [row[0] for row in reader if row]
116
+ except StopIteration:
117
+ return []
118
+ else:
119
+ return [line.strip() for line in f if line.strip()]
120
+ except Exception:
121
+ pass
122
+
123
+ # 3. Fallback to package resource (standard PEP 302/modern way)
124
+ try:
125
+ # Try finding it in the multiplayer.data subpackage first
126
+ try:
127
+ # We explicitly import the subpackage to ensure it's loaded
128
+ import multiplayer.data
129
+ package_path = resources.files(multiplayer.data)
130
+ # If we are looking for 'data/cities.csv', and we are in 'multiplayer.data',
131
+ # we just need 'cities.csv'
132
+ if 'data' in source_path.parts:
133
+ file_name = source_path.name
134
+ resource_path = package_path.joinpath(file_name)
135
+ else:
136
+ resource_path = package_path.joinpath(*source_path.parts)
137
+ except (ImportError, ModuleNotFoundError, ValueError):
138
+ # Fallback to main package
139
+ import multiplayer
140
+ package_path = resources.files(multiplayer)
141
+ resource_path = package_path.joinpath(*source_path.parts)
142
+
143
+ if resource_path.is_file():
144
+ with resource_path.open('r', encoding='utf-8') as f:
145
+ if str(source).endswith('.csv'):
146
+ reader = csv.reader(f)
147
+ try:
148
+ next(reader) # Assume header
149
+ return [row[0] for row in reader if row]
150
+ except StopIteration:
151
+ return []
152
+ else:
153
+ return [line.strip() for line in f if line.strip()]
154
+ except Exception:
155
+ pass
156
+
157
+ return None
158
+
159
+ def _suggest_from_category(category, valid_builtin_cats, valid_custom_cats):
160
+ """Internal helper to suggest a name from a specific category."""
161
+ if category in valid_custom_cats:
162
+ source = valid_custom_cats[category]
163
+ elif category in valid_builtin_cats:
164
+ source = valid_builtin_cats[category]
165
+ else:
166
+ return None
167
+
168
+ names = _get_names_from_source(source)
169
+ if not names:
170
+ return None
171
+ return random.choice(names)
172
+
173
+ def suggest_game_name(category=None):
174
+ """
175
+ Suggests a random game name.
176
+
177
+ If a category is provided, a name is chosen from that category.
178
+ If no category is provided, a random game-related category is chosen first.
179
+
180
+ Args:
181
+ category (str, optional): A category from get_available_categories("game").
182
+
183
+ Returns:
184
+ A string containing a random name, or None on failure.
185
+ """
186
+ if category:
187
+ return _suggest_from_category(category, _BUILTIN_GAME_CATEGORIES, _CUSTOM_GAME_CATEGORIES)
188
+
189
+ all_game_cats = {**_BUILTIN_GAME_CATEGORIES, **_CUSTOM_GAME_CATEGORIES}
190
+ if not all_game_cats:
191
+ return None
192
+ random_category = random.choice(list(all_game_cats.keys()))
193
+ return _suggest_from_category(random_category, _BUILTIN_GAME_CATEGORIES, _CUSTOM_GAME_CATEGORIES)
194
+
195
+ def suggest_player_name(category=None):
196
+ """
197
+ Suggests a random player name.
198
+
199
+ If a category is provided, a name is chosen from that category.
200
+ If no category is provided, a random player-related category is chosen first.
201
+
202
+ Args:
203
+ category (str, optional): A category from get_available_categories("player").
204
+
205
+ Returns:
206
+ A string containing a random name, or None on failure.
207
+ """
208
+ if category:
209
+ return _suggest_from_category(category, _BUILTIN_PLAYER_CATEGORIES, _CUSTOM_PLAYER_CATEGORIES)
210
+
211
+ all_player_cats = {**_BUILTIN_PLAYER_CATEGORIES, **_CUSTOM_PLAYER_CATEGORIES}
212
+ if not all_player_cats:
213
+ return None
214
+ random_category = random.choice(list(all_player_cats.keys()))
215
+ return _suggest_from_category(random_category, _BUILTIN_PLAYER_CATEGORIES, _CUSTOM_PLAYER_CATEGORIES)
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiplayer
3
+ Version: 0.11.0
4
+ Summary: Library that allows you to manage multiple players, locally, on a network, or on the Internet.
5
+ License-Expression: MIT
6
+ License-File: LICENSE.md
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Topic :: Games/Entertainment
14
+ Requires-Dist: colorlog
15
+ Requires-Dist: cryptography
16
+ Requires-Dist: pytest ; extra == 'dev'
17
+ Requires-Dist: requests ; extra == 'dev'
18
+ Requires-Dist: ruff ; extra == 'dev'
19
+ Requires-Python: >=3.12
20
+ Provides-Extra: dev
21
+ Description-Content-Type: text/markdown
22
+
23
+ **English** | [Español](translation/README.es.md) | [Français](translation/README.fr.md)
24
+
25
+ # Multiplayer Game Manager
26
+
27
+ > **A Note on this Project's Origin**
28
+ >
29
+ > This project is primarily the result of a series of experiments using Gemini Code Assist for code generation and error handling. Rather than using it on academic examples, it seemed more interesting to apply it to a project that could meet a real practical need.
30
+ >
31
+ > This, therefore, is the reason for `multiplayer`'s existence: you can dissect the code to see how Gemini (with my guidance) went about building it, or you can ignore all that and just use this library for your own needs!
32
+
33
+ This Python module provides a simple and flexible framework for managing multiplayer games, both locally and over a network.
34
+
35
+ For a detailed technical description of all classes and functions, see the [API Reference](REFERENCE.md).
36
+
37
+ ## Features
38
+
39
+ * **Local & Networked:** Use in a single process or in a client-server architecture.
40
+ * **Combined Game State:** A flexible system for synchronizing both the core game status (e.g., `in_progress`) and any custom game data.
41
+ * **Observer Support:** Ability to add observers who can view the game state without participating as players.
42
+ * **Administrator Role:** New `ServerAdmin` class to manage the server, kick players/observers, and monitor server status.
43
+ * **Game Grouping:** Organize several game sessions within the same server using the `GameGroup` class.
44
+ * **Multi-Layered Security:** Supports server passwords, admin passwords, and per-game passwords, with optional TLS v1.3 encryption. Passwords can be updated dynamically by administrators.
45
+ * **Automatic Server Discovery:** Clients can automatically find running servers on the local network.
46
+ * **Extensible Name Suggestions:** Includes a utility function to suggest creative names for games and players.
47
+ * **Multiple Games:** The server can manage multiple game sessions simultaneously, and the game list is now filtered to hide finished games.
48
+ * **Robust Error Handling:** A clear set of custom exceptions for both game logic and network issues.
49
+
50
+ ## Installation
51
+
52
+ You can install it in two ways:
53
+
54
+ ### 1. From PyPI
55
+ ```sh
56
+ pip install multiplayer
57
+ ```
58
+
59
+ ### 2. From a Wheel file (GitHub)
60
+ Download the `.whl` file from the [Releases](https://github.com/devfred78/multiplayer/releases) page and run:
61
+ ```sh
62
+ pip install multiplayer-0.11.0-py3-none-any.whl
63
+ ```
64
+ *Replace `multiplayer-0.11.0-py3-none-any.whl` with the actual name of the downloaded file.*
65
+
66
+ ## Usage
67
+
68
+ ### Game State Management
69
+
70
+ A key feature is the ability to manage your own game state alongside the core game status.
71
+
72
+ ```python
73
+ # On one client, set a custom state
74
+ game.set_state({
75
+ "board": [["X", "O", ""], ["", "X", ""], ["O", "", ""]],
76
+ "turn": "player2"
77
+ })
78
+
79
+ # On another client, retrieve the combined state
80
+ full_state = game.state
81
+ print(f"Game status: {full_state['status']}")
82
+ # > Game status: in_progress
83
+
84
+ print(f"Current turn: {full_state['custom']['turn']}")
85
+ # > Current turn: player2
86
+ ```
87
+
88
+
89
+ ### Game Grouping
90
+
91
+ You can group games together for better organization.
92
+
93
+ ```python
94
+ from multiplayer import Game, GameGroup
95
+
96
+ # Create a group
97
+ group = GameGroup("Tournament A", priority="high")
98
+
99
+ # Add games to the group
100
+ game1 = Game("Match 1")
101
+ game2 = Game("Match 2")
102
+ group.add_game(game1)
103
+ group.add_game(game2)
104
+
105
+ print(f"Game 1 ID: {game1.ID}")
106
+ # > Game 1 ID: 550e8400-e29b-41d4-a716-446655440000
107
+
108
+ print(f"Group '{group.name}' has {len(group.games)} games.")
109
+ # > Group 'Tournament A' has 2 games.
110
+ ```
111
+
112
+
113
+ ### Full Test Environment
114
+
115
+ A script is available to launch a complete test environment with:
116
+ - An IPC log server (`IPClogging`) in a separate window.
117
+ - A game server.
118
+ - Multiple separate client instances (default is 2) simulating a game, each in its own terminal window.
119
+
120
+ To run it:
121
+ ```bash
122
+ uv run python scripts/full_test_env.py
123
+ ```
124
+
125
+ To specify the number of players:
126
+ ```bash
127
+ uv run python scripts/full_test_env.py --players 3
128
+ ```
129
+ This will open several Windows Terminal windows: one for the log server and one for each client instance, allowing you to see the real-time interactions and logs.
130
+
131
+ ### Local Usage
132
+
133
+ You can use the `Game` class directly, including with a password for local validation.
134
+
135
+ ```python
136
+ from multiplayer import Game, Player, suggest_game_name
137
+
138
+ game = Game(name="My Awesome Game", password="local_game_pass")
139
+ game.add_player(Player("Alice"), password="local_game_pass")
140
+ game.start()
141
+ ```
142
+
143
+ ### Networked Usage (Client-Server)
144
+
145
+ #### Server Setup
146
+ ```python
147
+ from multiplayer import GameServer
148
+
149
+ # Start a secure server with a custom domain and self-signed certificate
150
+ server = GameServer(
151
+ host='0.0.0.0',
152
+ port=12345,
153
+ name="My Production Server",
154
+ password="my_server_password",
155
+ admin_password="my_admin_password",
156
+ use_tls=True,
157
+ tls_domain="example.com",
158
+ tls_self_signed=True
159
+ )
160
+ server.start()
161
+
162
+ # Or use existing certificate files
163
+ server = GameServer(
164
+ use_tls=True,
165
+ tls_cert="path/to/cert.pem",
166
+ tls_key="path/to/key.pem",
167
+ tls_self_signed=False
168
+ )
169
+ ```
170
+
171
+ #### Running with Docker
172
+
173
+ You can run the game server using Docker. To use your own TLS certificates, map a local directory containing `cert.pem` and `privkey.pem` to `/app/certs` in the container.
174
+
175
+ ```bash
176
+ docker run -d \
177
+ -p 65432:65432 \
178
+ -v /path/to/your/certs:/app/certs \
179
+ ghcr.io/yourusername/multiplayer-server:latest \
180
+ --name "My Docker Server" \
181
+ --use-tls --no-self-signed
182
+ ```
183
+
184
+ The server will automatically look for `cert.pem`, `RSA-cert.pem`, or `ECC-cert.pem` (and their corresponding keys) in the `/app/certs` directory.
185
+
186
+ #### Administrator Usage
187
+
188
+ ```python
189
+ from multiplayer import ServerAdmin
190
+
191
+ # Connect as administrator
192
+ admin = ServerAdmin(
193
+ host='localhost',
194
+ port=12345,
195
+ admin_password="my_admin_password",
196
+ use_tls=True
197
+ )
198
+
199
+ # Manage the server
200
+ info = admin.get_server_info()
201
+ print(f"Active games: {info['games_count']}")
202
+
203
+ # Check certificate expiration
204
+ expiration = admin.get_cert_expiration()
205
+ print(f"Certificate expires on: {expiration}")
206
+
207
+ # Kick a player if necessary
208
+ # admin.kick_player(game_id, player_id)
209
+
210
+ # Stop the server remotely
211
+ # admin.stop_server()
212
+ ```
213
+
214
+ #### Client Usage
215
+ ```python
216
+ from multiplayer import GameClient, Player, suggest_game_name
217
+
218
+ # 1. Discover and connect to the server
219
+ servers = GameClient.discover_servers()
220
+ if not servers:
221
+ print("No servers found.")
222
+ else:
223
+ host, port = servers[0]
224
+ client = GameClient(
225
+ host=host,
226
+ port=port,
227
+ password="my_server_password",
228
+ use_tls=True
229
+ )
230
+
231
+ # 2. Create a private game
232
+ private_game = client.create_game(
233
+ name=suggest_game_name(),
234
+ password="my_game_password"
235
+ )
236
+ print(f"Created game with ID: {private_game.ID}")
237
+
238
+ # 3. Create and use a Game Group
239
+ group = client.create_group("Tournament A")
240
+ game_in_group = group.create_game(name="Final Match")
241
+ print(f"Game in group '{group.name}' has ID: {game_in_group.ID}")
242
+
243
+ # 4. A player joins and sets the initial state
244
+ private_game.add_player(Player("Charlie"), password="my_game_password")
245
+ private_game.set_state({"score": 0})
246
+ private_game.start()
247
+ ```
248
+
249
+ ## Error Handling
250
+
251
+ The module provides a set of custom exceptions, including `AuthenticationError` for both server and game passwords.
252
+
253
+ ```python
254
+ from multiplayer import GameClient
255
+ from multiplayer.exceptions import ConnectionError, AuthenticationError
256
+
257
+ try:
258
+ # ... connect to client ...
259
+
260
+ # Try to join a game with the wrong password
261
+ game.add_player(Player("Eve"), password="wrong_game_password")
262
+
263
+ except AuthenticationError as e:
264
+ print(f"Authentication failed as expected: {e}")
265
+ except ConnectionError as e:
266
+ print(f"A connection or discovery error occurred: {e}")
267
+ ```
268
+
269
+ ## Contributing
270
+
271
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more details on how to get started.
272
+
273
+ ## Running Tests
274
+
275
+ To run the unit tests, you will need to have `pytest` installed.
276
+
277
+ ```sh
278
+ pip install pytest
279
+ ```
280
+
281
+ Then, you can run the tests from the root of the project:
282
+
283
+ ```sh
284
+ pytest
@@ -0,0 +1,31 @@
1
+ multiplayer/IPClogging/__init__.py,sha256=_0l2znu2eZsl7dYT6JCd23TQGwqMOfkTPLjtDOb-TwA,1590
2
+ multiplayer/IPClogging/echoing.py,sha256=pj8xGvvvxhRDE4dJXBip66vx5vyNWFYPmfee-fPc1cw,1153
3
+ multiplayer/IPClogging/server.py,sha256=E9STqw9QmkmQ0t4DnKKXK-verzwdY9IpAWXfcycczZ8,13753
4
+ multiplayer/IPClogging/test.py,sha256=SSgp6KQoZn6PnbLcCOiqlwAS_D70koSyMmfQWlcCxYQ,2335
5
+ multiplayer/__init__.py,sha256=DDXy3jtiqT378FI4fae9BU90L6BL_Q5tgLPeehdp3sI,1212
6
+ multiplayer/client.py,sha256=lzelylHYH0XbOhe0OwhonCnSdIqg5B-sVPhQ3fjY-qU,21428
7
+ multiplayer/data/__init__.py,sha256=IKVEo67OxQU1W-7JXrsuKCNA3Lc6wbcPbMYSlKsboPk,46
8
+ multiplayer/data/cities.csv,sha256=SQlwCxbofWxQN41NQUsybRsC5gAHvaDGkm7A7l9JlLQ,881
9
+ multiplayer/data/countries.csv,sha256=sB2MYdbNbKW2a6OaySeXNG4017dK6Pn5TQrKmt1s0V4,1351
10
+ multiplayer/data/egyptian_gods.csv,sha256=otF4bFk3P410W3nzXr7W-Byl6W5Bhmm2tVUpZRMK-Oo,696
11
+ multiplayer/data/european_kings.csv,sha256=i3fpfyK2KnPj8tzMO5DCB3r2XxCeKnFyXVmb6PqwQGs,1341
12
+ multiplayer/data/european_queens.csv,sha256=YxOfgU68HS3NktMu2ovDbmGwOmnGAjwUpctp5FHDuMM,1938
13
+ multiplayer/data/greek_gods.csv,sha256=D0yZG2TV8GpKVxMkcx-e8kP-vMvvbha9xRapx_ENbUo,917
14
+ multiplayer/data/planets_moons.csv,sha256=xBnp_DLsyoJX8BM48sKlQ_AR0CS0PJFVH14_B0GNaGs,909
15
+ multiplayer/data/rivers.csv,sha256=FK2l4Yt7RePcjQm48EfI_Ksxr4XhMKkyJVvSMYfz7LE,782
16
+ multiplayer/data/roman_gods.csv,sha256=KqzP5PcA68-Bj6hO7NefF6anb1ABzR5zvJ3MAXjJy88,819
17
+ multiplayer/data/seas_oceans.csv,sha256=VUzNRxEmjWePa7owC8GNnobOlbpEUUxEDG6KsJKZ3pA,1347
18
+ multiplayer/exceptions.py,sha256=mOYRbVRJU5W3wUORPPzsnbCI7xJ5DGDig1lUVhMkYmw,1052
19
+ multiplayer/game.py,sha256=Fup2BbTGrBvlUqZq8dW773HSUfYJ1G3Ak-6CXIphIOM,9020
20
+ multiplayer/language/__init__.py,sha256=uBWyC9rEkGtR5-_uGRqQWZ9ycH-NeO1L7JXzHJioKcg,869
21
+ multiplayer/language/language.py,sha256=5KcaDGAaGukMTYdw0JfuNuimpRmtWR0pBxkLG3A2mVo,21299
22
+ multiplayer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ multiplayer/run_log_server.py,sha256=FdQ_cTzxxEyeRYARGdxZHKEjD6cC9LmhMqNuacvb5Yw,915
24
+ multiplayer/run_server.py,sha256=l5y0tbnthpqUcQPCSKKVIcbEvBas_LdIm3GcwWJMf1g,3921
25
+ multiplayer/server.py,sha256=6b6T2-0TYGnxuoHEWmpB9GudAAxV7kfIYbhLcEh7JME,31217
26
+ multiplayer/utils.py,sha256=-kJ9qCx7BQ3GaTQlaoTbR6eqJP1_txYVuPRMP8v0r6A,7973
27
+ multiplayer-0.11.0.dist-info/licenses/LICENSE.md,sha256=kzBwgJsF0xF7Sn6fzdG1w10J1pUiaPZV_7qogwPxpd4,1161
28
+ multiplayer-0.11.0.dist-info/WHEEL,sha256=q5IF0q2xCp3ktUFRCVWsQLjl2ChNlWXBJtnI1LCGdJ8,80
29
+ multiplayer-0.11.0.dist-info/entry_points.txt,sha256=17_5t71OVCgGXdxI8bEfqjCFPUkn6PWcCF9CshY_cKg,125
30
+ multiplayer-0.11.0.dist-info/METADATA,sha256=92VkL11dX-CD85jr4FMHfs-sCCMjH0WRSCFHx4sKPo4,8861
31
+ multiplayer-0.11.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.8
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ multiplayer-log-server = multiplayer.run_log_server:main
3
+ multiplayer-server = multiplayer.run_server:main
4
+
@@ -0,0 +1,23 @@
1
+ **English** | [Español](translation/LICENSE.es.md) | [Français](translation/LICENSE.fr.md)
2
+
3
+ # MIT License
4
+
5
+ Copyright (c) 2026 devfred78
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify,merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT of OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.