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/IPClogging/__init__.py +42 -0
- multiplayer/IPClogging/echoing.py +34 -0
- multiplayer/IPClogging/server.py +338 -0
- multiplayer/IPClogging/test.py +71 -0
- multiplayer/__init__.py +52 -0
- multiplayer/client.py +531 -0
- multiplayer/data/__init__.py +1 -0
- multiplayer/data/cities.csv +103 -0
- multiplayer/data/countries.csv +152 -0
- multiplayer/data/egyptian_gods.csv +110 -0
- multiplayer/data/european_kings.csv +109 -0
- multiplayer/data/european_queens.csv +105 -0
- multiplayer/data/greek_gods.csv +122 -0
- multiplayer/data/planets_moons.csv +123 -0
- multiplayer/data/rivers.csv +103 -0
- multiplayer/data/roman_gods.csv +109 -0
- multiplayer/data/seas_oceans.csv +104 -0
- multiplayer/exceptions.py +39 -0
- multiplayer/game.py +275 -0
- multiplayer/language/__init__.py +23 -0
- multiplayer/language/language.py +445 -0
- multiplayer/py.typed +0 -0
- multiplayer/run_log_server.py +33 -0
- multiplayer/run_server.py +91 -0
- multiplayer/server.py +676 -0
- multiplayer/utils.py +215 -0
- multiplayer-0.11.0.dist-info/METADATA +284 -0
- multiplayer-0.11.0.dist-info/RECORD +31 -0
- multiplayer-0.11.0.dist-info/WHEEL +4 -0
- multiplayer-0.11.0.dist-info/entry_points.txt +4 -0
- multiplayer-0.11.0.dist-info/licenses/LICENSE.md +23 -0
|
@@ -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()
|