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.
@@ -0,0 +1,445 @@
1
+ """
2
+ multiplayer, a Python library for managing multiplayer games
3
+ Copyright (C) 2025 [devfred78](https://github.com/devfred78)
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ This module provides tools for managing interface languages.
19
+ """
20
+
21
+ import logging
22
+ from pathlib import Path
23
+ import tomllib
24
+ import sys
25
+
26
+ class Language():
27
+ """
28
+ The set of texts that can be displayed on the interface, in a given language, instantiated from a string or a file in TOML format.
29
+
30
+ During instantiation, a check is performed to ensure that only elements that exist in the `pattern` are taken into account. If additional elements are present, they are silently ignored. On the other hand, import is possible even if the set of imported elements does not completely cover the `pattern` keys.
31
+
32
+ Parameters:
33
+ file:
34
+ The file or string from which data will be imported. The file can be a pathlib.Path object, or a string describing the relative or absolute path to the file to be imported. It can also be a string containing text in [TOML format](https://toml.io).
35
+
36
+ ```pycon
37
+ >>> from language import Language
38
+ >>> from pathlib import Path
39
+ >>>
40
+ >>> # The following commands are equivalent:
41
+ >>>
42
+ >>> language1 = Language('/path/to/file/language.lng')
43
+ >>>
44
+ >>> my_file = Path('/path/to/file/language.lng')
45
+ >>> language2 = Language(my_file)
46
+ >>>
47
+ >>> my_str = '''
48
+ [header]
49
+ programme = "Programme Name"
50
+ version = "0.1"
51
+ language = "fr"
52
+ [default]
53
+ area = "FR"
54
+ [FR]
55
+ WHOAMI = "Qui suis-je ?"
56
+ '''
57
+ >>> language3 = Language(my_str)
58
+ ```
59
+ pattern:
60
+ a dict object whose keys will be used to form the instance from the language file. When `pattern` is not empty, only those elements of the loaded file that correspond to the keys will actually be used. If it is empty (default), all elements of the loaded file will be used.
61
+
62
+ Examples:
63
+ If
64
+
65
+ >>> `pattern` = `{"WHOAMI": "Who am I ?", "NAME": "Name"}
66
+
67
+ and the language file is
68
+
69
+ >>> my_lang = '''
70
+ ...
71
+ [FR]
72
+ WHOAMI = "Qui suis-je ?"
73
+ NAME = "Nom"
74
+ DEFAULT_NAME = "Joueur"
75
+ ...
76
+ '''
77
+
78
+ then, only WHOAMI and NAME will be loaded, and not DEFAULT_NAME.
79
+ logger:
80
+ The parent logger used to track events that append when the instance is running. Mainly for status monitoring or fault investigation purposes. If None (the default), no event is tracked.
81
+
82
+ Attributes:
83
+ header:
84
+ Dict object containing the `header` section of the language file. **read-only**. It is normally composed of the following indexes:
85
+
86
+ | Index | value |
87
+ | ----------- | ----------------------------------------------------------------------------- |
88
+ | `programme` | Sring containing the name of programme on which the translation is applied |
89
+ | `version` | String containing the version of the language file, |
90
+ | | following the [Semantic Versioning Specification](https://semver.org/) |
91
+ | `language` | 2-character string representing the language supported by the imported file, |
92
+ | | following the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code) |
93
+ usable:
94
+ `True` if the language file has been successfully loaded and decoded, and the Language instance is fully available. `False` otherwise. **read-only**
95
+ countries:
96
+ Tuple of all countries covered by the language file, represented by a 2-character string following the [ISO 3166 alpha-2 standard](https://www.iso.org/iso-3166-country-codes.html)
97
+ all_translations:
98
+ Dict object containing supported countries as keys, and for each, a dict object for the translated elements with the following format {"key 1 in pattern" : "translation 1 found in the language file", "key 2 in pattern": "translation 2 found in the langauge file", ...}. **read-only**
99
+ default_country:
100
+ 2-character string representing the default country to be used as key in the all_translations attribute to find the default dictionnary of translated elements. **read-only**
101
+ log:
102
+ The logger used to track events that append when the instance is running. Mainly for status monitoring or fault investigation purposes.
103
+
104
+ Raises:
105
+ FileNotFoundError:
106
+ Raises if `file` seems to be a file (`Path` object or a path indicated in a string) but the file actually does not exist.
107
+ ValueError:
108
+ Raises if `file` cannot be decoded, either as a string or as an existing `Path` object (but whose content is invalid).
109
+ TypeError:
110
+ Raises if `file` is neither a string nor a `Path` object.
111
+ """
112
+ def __init__(self, file:Path|str, pattern:dict = {}, logger:logging.Logger = None):
113
+ # Language instance initialization
114
+
115
+ if logger is None:
116
+ self.log = logging.getLogger("Language")
117
+ self.log.addHandler(logging.NullHandler())
118
+ else:
119
+ self.log = logger.getChild("Language")
120
+
121
+ self.log.debug("--- Language initialization ---")
122
+
123
+ self._pattern = pattern
124
+ self._header: dict = {}
125
+ self._usable: bool = False
126
+ self._all_translations: dict = {}
127
+ self._default_country: str = ""
128
+
129
+ if isinstance(file, Path):
130
+ if file.is_file():
131
+ try:
132
+ self._load_lng_dict(self._load_Path_file(file))
133
+ except tomllib.TOMLDecodeError:
134
+ raise ValueError
135
+ else:
136
+ self.log.warning(f"File {file.name} is not present. The expected associated language will not be applied.")
137
+ raise FileNotFoundError
138
+ elif isinstance(file, str):
139
+ try:
140
+ my_file = Path(file)
141
+ except Exception:
142
+ try:
143
+ self._load_lng_dict(self._parse_str_lng(file))
144
+ except tomllib.TOMLDecodeError:
145
+ raise ValueError
146
+ else:
147
+ if my_file.is_file():
148
+ try:
149
+ self._load_lng_dict(self._load_Path_file(file))
150
+ except tomllib.TOMLDecodeError:
151
+ raise ValueError
152
+ else:
153
+ raise FileNotFoundError
154
+ else:
155
+ raise TypeError
156
+
157
+ @property
158
+ def header(self) -> dict:
159
+ # Dict object containing the `header` section of the language file. **read-only**
160
+ return self._header
161
+
162
+ @property
163
+ def usable(self) -> bool:
164
+ # `True` if the language file has been successfully loaded and decoded, and the Language instance is fully available. `False` otherwise. **read-only**
165
+ return self._usable
166
+
167
+ @property
168
+ def all_translations(self) -> dict:
169
+ # Dict object containing supported countries as keys, and for each, a dict object for the translated elements
170
+ return self._all_translations
171
+
172
+ @property
173
+ def default_country(self) -> str:
174
+ # 2-character string representing the default country to be used as key in the all_translations attribute to find the default dictionnary of translated elements
175
+ return self._default_country
176
+
177
+ def _load_Path_file(self, file:Path) -> dict:
178
+ """
179
+ Loads the language file into a dict object.
180
+
181
+ The file must be in TOML format.
182
+
183
+ Parameters:
184
+ file:
185
+ The file from which data will be imported.
186
+
187
+ Returns:
188
+ dict: a representation of the language file in a dict object
189
+ """
190
+ with open(file, "rb") as lang_file:
191
+ try:
192
+ data = tomllib.load(lang_file)
193
+ except tomllib.TOMLDecodeError:
194
+ self.log.warning(f"Language File {file.name} does not respect the expected TOML format. It will be not loaded.")
195
+ self._usable = False
196
+ raise
197
+ else:
198
+ self.log.debug(f"Language file {file.name} successfully loaded.")
199
+ self._usable = True
200
+ return data
201
+
202
+ def _parse_str_lng(self, str_lng:str) -> dict:
203
+ """
204
+ Parses a TOML-compliant string, which is supposed to be the content of a language file, and loads it into a dict object.
205
+
206
+ Parameters:
207
+ str_lng:
208
+ The string from which data will be imported.
209
+
210
+ Returns:
211
+ dict: a representation of the language string in a dict object
212
+ """
213
+ try:
214
+ data = tomllib.loads(str_lng)
215
+ except tomllib.TOMLDecodeError:
216
+ self.log.warning("The provided string does not respect the expected TOML format. It will be not loaded.")
217
+ self._usable = False
218
+ raise
219
+ else:
220
+ self.log.debug("Provided string successfully loaded.")
221
+ self._usable = True
222
+ return data
223
+
224
+ def _load_lng_dict(self, data:dict):
225
+ """
226
+ Loads a dict object supposed to be an extraction of a language file in TOML format, into the relevant attributes of the current instance.
227
+
228
+ During loading, a check is performed with the `pattern` given as parameter for the instanciation (see description of this parameter at the class level).
229
+
230
+ Parameters:
231
+ data:
232
+ Extraction of the language file
233
+
234
+ Returns:
235
+ None
236
+ """
237
+
238
+ # Reading the header
239
+ try:
240
+ self._header = data['header']
241
+ except KeyError:
242
+ self.log.warning("Header not present in the language file. Language file not usable.")
243
+ self._usable = False
244
+ raise tomllib.TOMLDecodeError
245
+
246
+ # Reading the supported countries
247
+ self.countries = tuple([index for index in data.keys() if index not in ['header', 'default']])
248
+ self.log.debug(f"The language file supports the following country code{"" if len(self.countries) <= 1 else "s"}: {self.countries}")
249
+
250
+ # Reading the translated elements for each country
251
+ for country in self.countries:
252
+ imported_elements = {}
253
+ if len(self._pattern.keys()) != 0:
254
+ for element in self._pattern.keys():
255
+ try:
256
+ imported_elements[element] = data[country][element]
257
+ except KeyError:
258
+ self.log.warning(f"'{element}' not found in the language file for the country {country}")
259
+ else:
260
+ imported_elements = data[country].copy()
261
+ self._all_translations[country] = imported_elements.copy()
262
+
263
+ # Reading the default country or region
264
+ try:
265
+ self._default_country = data['default']['country']
266
+ except KeyError:
267
+ self.log.warning("The language file has no default country")
268
+ self._default_country = self.countries[0]
269
+ self.log.warning(f"{self._default_country} is used as default country for the language {self.header['language']}")
270
+ else:
271
+ self.log.debug(f"{self._default_country} is the default country for the language {self.header['language']}")
272
+
273
+ class Languages():
274
+ """
275
+ All languages supported for the interface, with all associated texts.
276
+
277
+ During instantiation, all compatible language files in the specified directory are automatically recognized and loaded. It is also possible to add and remove a `Language` object that has been initialized before.
278
+
279
+ To retrieve a `dict` object containing all translated texts, use the form `Languages[key]`, where `key` is a character string of the following form:
280
+
281
+ `<lang>_<country>`
282
+
283
+ with `<lang>` a 2-character string specifying the language, according to the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code), and `<country>` a 2-character string specifying the geographical area, according to the [ISO 3166 alpha-2 standard](https://www.iso.org/iso-3166-country-codes.html).
284
+ The underscore character `_` can be replaced by any other character; it carries no meaning other than that of separator.
285
+
286
+ If `<country>` is not specified, or if it doesn't correspond to any geographical area relative to the specified language, then the `dict` object returned corresponds to the default geographical area of the language in question.
287
+
288
+ If the first 2 characters of `key` do not correspond to any language supported by the `Languages` instance, then `KeyError` is raised.
289
+
290
+ Parameters:
291
+ directory:
292
+ The directory where the language files to be loaded are searched. By default, this will be the current directory.
293
+ extension:
294
+ The language file extension, including the dot (.). The default is '.lng'.
295
+ pattern:
296
+ a dict object whose keys will be used to form the instance from the language files. When `pattern` is not empty, only those elements of the loaded file that correspond to the keys will actually be used. If it is empty (default), all elements of the loaded file will be used.
297
+
298
+ Examples:
299
+ If
300
+
301
+ >>> `pattern` = `{"WHOAMI": "Who am I ?", "NAME": "Name"}
302
+
303
+ and the language file is
304
+
305
+ >>> my_lang = '''
306
+ ...
307
+ [FR]
308
+ WHOAMI = "Qui suis-je ?"
309
+ NAME = "Nom"
310
+ DEFAULT_NAME = "Joueur"
311
+ ...
312
+ '''
313
+
314
+ then, only WHOAMI and NAME will be loaded, and not DEFAULT_NAME.
315
+ logger:
316
+ The parent logger used to track events that append when the instance is running. Mainly for status monitoring or fault investigation purposes. If None (the default), no event is tracked.
317
+
318
+ Attributes:
319
+ supported_languages_iso639:
320
+ Tuple of 2-character strings representing the supported languages, following the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code). **Read-only**.
321
+
322
+ Raises:
323
+ KeyError:
324
+ Raises when `Languages[key]` is called and `key` does not provide a language supported by the instance.
325
+ """
326
+ if getattr(sys, "frozen", False):
327
+ DEFAULT_DIR = Path(sys.executable).parent
328
+ else:
329
+ DEFAULT_DIR = Path(__file__).resolve().parent
330
+
331
+ def __init__(self, directory:Path = DEFAULT_DIR, extension:str = '.lng', pattern:dict = {}, logger:logging.Logger = None):
332
+ # Languages instance initialization
333
+
334
+ if logger is None:
335
+ self.log = logging.getLogger("Languages")
336
+ self.log.addHandler(logging.NullHandler())
337
+ else:
338
+ self.log = logger.getChild("Languages")
339
+
340
+ self.log.debug("--- Languages initialization ---")
341
+
342
+ self._pattern = pattern
343
+ self._supported_languages = []
344
+
345
+ if directory.is_dir():
346
+ for file in directory.iterdir():
347
+ if file.is_file() and (file.suffix.lower() == extension.lower()):
348
+ self.add(file)
349
+ else:
350
+ self.log.warning(f"{directory} is not a proper directory.")
351
+
352
+ def __getitem__(self, key:str) -> dict:
353
+ # Implementation evaluation of self[key]
354
+ lang = str(key).lower()[:2]
355
+ if len(key) > 2:
356
+ country = str(key).lower()[-2:]
357
+ else:
358
+ country = "no_country"
359
+ if lang in self.supported_languages_iso639:
360
+ eligible_languages = [languages for languages in self._supported_languages if lang == languages.header['language'].lower()]
361
+ if len(eligible_languages) > 0:
362
+ language = eligible_languages[0]
363
+ if country in self.supported_countries_iso3166(lang):
364
+ return language.all_translations[country]
365
+ else:
366
+ return language.all_translations[language.default_country]
367
+ raise KeyError(f"{lang} is not a supported language.")
368
+
369
+ @property
370
+ def supported_languages_iso639(self) -> tuple:
371
+ # Tuple of 2-character strings representing the supported languages, following the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code). **Read-only**.
372
+ return tuple([language.header['language'].lower() for language in self._supported_languages])
373
+
374
+
375
+ def add(self, lng_file:Path|str):
376
+ """
377
+ Adds a new language file.
378
+
379
+ The file is checked before loading, so that only compatible language files are integrated. Once a pattern has been defined, it is used to retrieve the necessary translations.
380
+
381
+ Parameters:
382
+ lng_file:
383
+ The file or string from which data will be imported. The file can be a pathlib.Path object, or a string describing the relative or absolute path to the file to be imported. It can also be a string containing text in [TOML format](https://toml.io).
384
+
385
+ Returns:
386
+ None
387
+ """
388
+ try:
389
+ language = Language(lng_file, self._pattern, self.log)
390
+ except FileNotFoundError:
391
+ self.log.warning(f"{lng_file} file was not found.")
392
+ except ValueError:
393
+ self.log.warning(f"{lng_file} file cannot be properly decoded.")
394
+ except TypeError:
395
+ self.log.warning(f"{lng_file} is neither a string nor a `Path` object.")
396
+ else:
397
+ if language.usable:
398
+ self._supported_languages.append(language)
399
+ self.log.info(f"Language '{language.header['language']}' is properly supported, with the following variation(s): {language.countries}.")
400
+ else:
401
+ self.log.warning(f"{lng_file} file is decoded, but not usable.")
402
+
403
+ def remove(self, lng:str):
404
+ """
405
+ Removes a language previously added.
406
+
407
+ If the given language is not included in the instance, the function is silently ignored. Please notice that all countries of the language are removed too.
408
+
409
+ Parameters:
410
+ lng:
411
+ 2-character string representing the language supported by the imported file, following the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code)
412
+
413
+ Returns:
414
+ None
415
+ """
416
+ if lng.lower() in self.supported_languages_iso639:
417
+ for language in [languages for languages in self._supported_languages if languages.header['language'].lower() == lng.lower()]:
418
+ try:
419
+ self._supported_languages.remove(language)
420
+ except ValueError:
421
+ self.log.debug(f"{lng.lower()} present in `supported_languages_iso639` but there is no corresponding Language object")
422
+ return
423
+ self.log.debug(f"{lng.lower()} successfully removed.")
424
+ else:
425
+ self.log.debug(f"No {lng.lower()} language found: removal is not possible.")
426
+
427
+
428
+ def supported_countries_iso3166(self, language_iso639:str) -> tuple:
429
+ """
430
+ Provides all countries covered by the given language.
431
+
432
+ If the given language is not supported, the returned tuple is empty.
433
+
434
+ Parameters:
435
+ language_iso639:
436
+ 2-character string representing the language supported by the imported file, following the [ISO 639.1 standard](https://www.iso.org/iso-639-language-code)
437
+
438
+ Returns:
439
+ Tuple of all countries covered by the language, represented by a 2-character string following the [ISO 3166 alpha-2 standard](https://www.iso.org/iso-3166-country-codes.html)
440
+ """
441
+ if language_iso639.lower() in self.supported_languages_iso639:
442
+ for language in self._supported_languages:
443
+ if language.header['language'].lower() == language_iso639.lower():
444
+ return language.countries
445
+ return tuple()
multiplayer/py.typed ADDED
File without changes
@@ -0,0 +1,33 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from multiplayer.IPClogging.server import server
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="Launch a standalone logging server.")
8
+ parser.add_argument(
9
+ "--port",
10
+ type=int,
11
+ default=5000,
12
+ help="Port to listen on (default: 5000)"
13
+ )
14
+ parser.add_argument(
15
+ "--color-mode",
16
+ choices=["level", "origin"],
17
+ default="level",
18
+ help="Coloration mode: 'level' (by criticality) or 'origin' (by message source)"
19
+ )
20
+
21
+ args = parser.parse_args()
22
+
23
+ try:
24
+ print(f"Starting standalone logging server on port {args.port}...")
25
+ server(args.port, color_mode=args.color_mode)
26
+ except KeyboardInterrupt:
27
+ print("\nLogging server stopped by user.")
28
+ except Exception as e:
29
+ print(f"\nAn error occurred: {e}")
30
+ sys.exit(1)
31
+
32
+ if __name__ == "__main__":
33
+ main()
@@ -0,0 +1,91 @@
1
+ import argparse
2
+ import sys
3
+ import time
4
+ import os
5
+ from multiplayer.server import GameServer
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Run a multiplayer game server.")
9
+ parser.add_argument("--host", default="0.0.0.0", help="Host to listen on (default: 0.0.0.0)")
10
+ parser.add_argument("--port", type=int, default=65432, help="Port to listen on (default: 65432)")
11
+ parser.add_argument("--password", help="Server password")
12
+ parser.add_argument("--admin-password", help="Admin password")
13
+ parser.add_argument("--use-tls", action="store_true", help="Enable TLS")
14
+ parser.add_argument("--tls-domain", default="localhost", help="Domain name for TLS certificate (default: localhost)")
15
+ parser.add_argument("--tls-cert", help="Path to TLS certificate (.pem)")
16
+ parser.add_argument("--tls-key", help="Path to TLS private key (.pem)")
17
+ parser.add_argument("--tls-cert-dir", help="Directory containing TLS certificate (cert.pem) and key (privkey.pem)")
18
+ parser.add_argument("--tls-self-signed", action="store_true", default=True, help="Generate a self-signed certificate (default: True)")
19
+ parser.add_argument("--no-self-signed", action="store_false", dest="tls_self_signed", help="Do not generate a self-signed certificate")
20
+ parser.add_argument("--logging-host", help="IPC logging server host")
21
+ parser.add_argument("--logging-port", type=int, help="IPC logging server port")
22
+ parser.add_argument("--logger-name", default="GameServer", help="Name of the logger (default: GameServer)")
23
+ parser.add_argument("--name", help="Human-readable name for the server instance")
24
+
25
+ args = parser.parse_args()
26
+
27
+ tls_cert = args.tls_cert
28
+ tls_key = args.tls_key
29
+ tls_self_signed = args.tls_self_signed
30
+
31
+ if args.tls_cert_dir:
32
+ print(f"Scanning directory for certificates: {args.tls_cert_dir}")
33
+ if not os.path.isdir(args.tls_cert_dir):
34
+ print(f"Error: {args.tls_cert_dir} is not a directory.")
35
+ sys.exit(1)
36
+
37
+ # Look for cert.pem/privkey.pem first, then others
38
+ potential_certs = ["cert.pem", "RSA-cert.pem", "ECC-cert.pem"]
39
+ potential_keys = ["privkey.pem", "RSA-privkey.pem", "ECC-privkey.pem"]
40
+
41
+ found_cert = None
42
+ for c in potential_certs:
43
+ p = os.path.join(args.tls_cert_dir, c)
44
+ if os.path.isfile(p):
45
+ found_cert = p
46
+ break
47
+
48
+ found_key = None
49
+ for k in potential_keys:
50
+ p = os.path.join(args.tls_cert_dir, k)
51
+ if os.path.isfile(p):
52
+ found_key = p
53
+ break
54
+
55
+ if found_cert and found_key:
56
+ print(f"Found certificates in {args.tls_cert_dir}: {os.path.basename(found_cert)}, {os.path.basename(found_key)}")
57
+ tls_cert = found_cert
58
+ tls_key = found_key
59
+ tls_self_signed = False
60
+ else:
61
+ print(f"Warning: Could not find both a certificate and a key in {args.tls_cert_dir}. Falling back to other options.")
62
+
63
+ server = GameServer(
64
+ host=args.host,
65
+ port=args.port,
66
+ password=args.password,
67
+ admin_password=args.admin_password,
68
+ use_tls=args.use_tls,
69
+ tls_domain=args.tls_domain,
70
+ tls_cert=tls_cert,
71
+ tls_key=tls_key,
72
+ tls_self_signed=tls_self_signed,
73
+ logging_host=args.logging_host,
74
+ logging_port=args.logging_port,
75
+ logger_name=args.logger_name,
76
+ name=args.name
77
+ )
78
+
79
+ try:
80
+ server.start()
81
+ # Keep the main thread alive while the server process is running
82
+ while True:
83
+ time.sleep(1)
84
+ if server._server_process and not server._server_process.is_alive():
85
+ break
86
+ except KeyboardInterrupt:
87
+ print("\nStopping server...")
88
+ server.stop()
89
+ except EOFError:
90
+ print("\nStopping server...")
91
+ server.stop()