Compare commits

..

13 Commits

Author SHA1 Message Date
a9dad5ab8f Add codes to _looper();
Rewrite _tcp_send(to_all=True)
2023-07-15 19:00:21 +03:00
aa5725e8a5 Minor fix 2023-07-15 18:03:22 +03:00
939723acdd Add event player_join 2023-07-15 17:54:59 +03:00
90beaf1302 Refactor class Client to protected funcs and vars 2023-07-15 17:54:08 +03:00
ee366a2d23 Update TODOs 2023-07-15 17:04:12 +03:00
d665021479 i18n: Update en translate 2023-07-15 17:01:58 +03:00
13ff3207b2 Update en docs 2023-07-15 16:32:07 +03:00
50b479c396 Update docs 2023-07-15 16:21:43 +03:00
62fa4c6f25 Update docs 2023-07-15 16:20:37 +03:00
f0f8da962e Update async logic;
New except;
2023-07-15 15:37:55 +03:00
840d8fd685 New except 2023-07-15 15:36:59 +03:00
4629fbc43a Update Async logic 2023-07-15 15:36:45 +03:00
e9919459af Update Async logic 2023-07-15 15:34:24 +03:00
26 changed files with 792 additions and 348 deletions

View File

@ -36,7 +36,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [ ] _(Deferred)_ Static text (bug)
- [x] _~~(By design)~~_ Static text (bug)
- [x] Events System
- [x] Call events
- [x] Create custom events
@ -56,7 +56,7 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] [Documentation](docs/en/readme.md)
- [ ] [Documentation](./docs/)
## Installation

View File

@ -1,15 +1,15 @@
{
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"hello": "Greetings from KuiToi Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization complete.",
"start": "Server started!",
"stop": "Goodbye!",
"stop": "Server stopped!",
"": "Server auth",
"auth_need_key": "BEAM key needed for starting the server!",
"auth_empty_key": "Key is empty!",
"auth_cannot_open_browser": "Cannot open browser: {}",
"auth_need_key": "A BeamMP key is required to start the server!",
"auth_empty_key": "The BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"": "GUI phases",
@ -17,32 +17,32 @@
"GUI_no": "No",
"GUI_ok": "Ok",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
"GUI_enter_key_message": "Please type your key:",
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
"": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.",
"man_for": "Manual for command",
"man_message_not_found": "man: Manual message not found.",
"man_command_not_found": "man: command \"{}\" not found!",
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Displays help page for COMMAND.",
"man_for": "Help page for",
"man_message_not_found": "man: Help page not found.",
"man_command_not_found": "man: Command \"{}\" not found!",
"": "Command: help",
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
"help_message_help": "Display names and brief descriptions of available commands",
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
"help_message_help": "Displays the names and short descriptions of commands.",
"help_command": "Command",
"help_message": "Help message",
"help_message_not_found": "No help message found",
"help_message": "Description",
"help_message_not_found": "No description available.",
"": "Command: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
"help_message_stop": "Server shutdown.",
"man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
"help_message_exit": "Server shutdown."
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}

View File

@ -0,0 +1,4 @@
# MultiLanguage - i18n Support
In [example.json](./example.json) you will find a copy of [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json).\
If you want to translate to a language that has not been translated before or update an existing translation, I would be happy to receive your pull requests.

View File

@ -0,0 +1,37 @@
import json
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
cfg_file = "config.json"
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# Инициализация плагина
with open(cfg_file, 'w') as f:
json.dump(config, f)
cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
async def start():
# Запуск процессов плагина
await ev.call_async_event("my_event")
await ev.call_async_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
async def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")

View File

@ -1,36 +1,37 @@
import KuiToi # Import server object
import json
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
log = beam.log # Use logger from server
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
cfg_file = "config.json"
def on_load():
# When plugin initialization Server uses plugin.load() to load plugin.
# def load(): is really needed
log.info(beam.name)
def my_event_handler(event_data):
log.info(f"{event_data}")
# Events handlers
def on_started():
# Simple event handler
log.info("Server starting...")
def load():
# Инициализация плагина
with open(cfg_file, 'w') as f:
json.dump(config, f)
cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
# Simple event register
beam.register_event("on_started", on_started)
def start():
# Запуск процессов плагина
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
def any_func(data=None):
# Custom event handler
log.info(f"Data from any_func: {data}")
# Create custom event
beam.register_event("my_event", any_func)
# Call custom event
beam.call_event("my_event")
beam.call_event("my_event", "Some data")
# This will be an error since any_func accepts only one argument at the input
beam.call_event("my_event", "Some data", "Some data1")
def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")

View File

@ -1,32 +1,92 @@
# Plugins System
# Plugin System
## Install
###### (Lib can't ready to use)
## Installing the Library with "Stubs"
###### (This means that it will not work without a server, but the IDE will suggest the API)
###### (The library is still under development)
* From pip:\
* Using pip:\
`$ pip install KuiToi`
* From source:\
* From source code:\
`git clone https://github.com/KuiToi/KuiToi-PyLib`
## Example
```python
import KuiToi
try:
import KuiToi
except ImportError:
pass
beam = KuiToi("TestPlugin")
logger = beam.log
kt = KuiToi("Example")
log = kt.log
def load(): # Plugins load from here
print(beam.name)
def my_event_handler(event_data):
log.info(f"{event_data}")
def on_started():
logger.info("Server starting...")
def load():
# Plugin initialization
ev.register_event("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
beam.register_event("on_started", on_started)
def start():
# Running plugin processes
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Plugin started successfully.")
def unload():
# Code that ends all processes
log.info("Plugin unloaded successfully.")
```
* Basic Events: ['on_started', 'on_auth, 'on_stop']
* Create new event : `beam.register_event("my_event", my_event_function)`
* Call event: `beam.call_event("my_event")`
* Call event with some data: `beam.call_event("my_event", data, data2)`
* Calls _**can't support**_ like this: `beam.call_event("my_event", data=data)`
* It is recommended to use `open()` after `load()`. Otherwise, use `kt.load()` - creates a file in the `plugin/<plugin_name>/<filename>` folder.
* Creating your own event: `kt.register_event("my_event", my_event_function)`
* Calling an event: `kt.call_event("my_event")`
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
* Basic events: _Will write later_
## Async Functions
Async support is available.
```python
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# Plugin initialization
ev.register_event("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
async def start():
# Running plugin processes
await ev.call_async_event("my_event")
await ev.call_async_event("my_event", "Some data", data="some data too")
log.info("Plugin started successfully.")
async def unload():
# Code that ends all processes
log.info("Plugin unloaded successfully.")
```
A more extensive example can also be found in [async_example.py](./async_example.py).
* Creating your own event: `kt.register_event("my_event", my_event_function)` (register_event checks for function)
* Calling an async event: `kt.call_async_event("my_event")`
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
* Basic async events: _Will write later_

View File

@ -1,9 +1,9 @@
# Documentation for KuiToi Server
#### The documentation has not been perfected yet, but one day it will definitely happen
### The documentation is not perfect yet, but it will be one day
1. Setup and Start server - [here](setup)
2. Plugins and Events system - [here](plugins)
3. MultiLanguage - [here](./multilanguage)
4. RESP API - [here](./web)
5. Something new
4. KuiToi WebAPI - [here](./web)
5. Something new...

View File

@ -1,18 +1,32 @@
# Hello from KuiToi Server
# Greetings from KuiToi Server
## Start
## Well, let's begin
* Need **Python 3.10.x** to start!
* After cloning use this:
###### _(Here are the commands for Linux)_
* **Python 3.10.x** is required to run the server! It won't work on Python 3.11...
* You can check the version of your Python installation with the following command:
```bash
$ python3 --version # Python 3.10.6
$ python3 main.py --help # Show help message
$ python3 main.py # Start server
python3 --version # Python 3.10.6
```
* Clone the repository and navigate to it.
* Install everything that's needed.
* Then, using my "script", remove all unnecessary files and move to the core source code.
```bash
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
pip install -r requirements.txt
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
```
* Here's how to view information about the server and start it:
```bash
python3 main.py --help # Displays all available commands
python3 main.py # Starts the server
```
## Setup
## Configuration
* After starting server creating `kuitoi.yaml`; Default:
* After starting the server, a `kuitoi.yaml` file will be created.
* By default, it looks like this:
```yaml
!!python/object:modules.ConfigProvider.config_provider.Config
Auth:
@ -23,11 +37,39 @@ Game:
max_cars: 1
players: 8
Server:
debug: true
description: This server uses KuiToi!
debug: false
description: Welcome to KuiToi Server!
language: en
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
```
* Server can't start without BEAM Auth.key
server_port: 30813
WebAPI:
enabled: false
secret_key: <random_key>
server_ip: 127.0.0.1
server_port: 8433
```
### Auth
* If you set `private: false` and do not set a `key`, the server will request a BeamMP key and will not start without it.
* By entering a BeamMP key, the server will appear in the launcher list.
* You can get a key here: [https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
### Game
* `map` specifies only the name of the map. That is, open the mod with the map in `map.zip/levels` - the name of the map will be there, and that's what you need to insert.
* `max_cars` - the maximum number of cars per player
* `players` - the maximum number of players
### Server
* `debug` - should debug messages be displayed (for experienced users only; slightly affects performance)
* `description` - server description for the BeamMP launcher
* `language` - the language in which the server will run (currently available: en, ru)
* `name` - server name for the BeamMP launcher
* `server_ip` - the IP address to be used by the server (for experienced users only; defaults to 0.0.0.0)
* `server_port` - the port on which the server will run
### WebAPI
##### _Docs are not ready yet_

View File

@ -1,3 +1,15 @@
# Web RESP API for the Server
Here's the translation of the readme.txt content:
In development...
# WebAPI for the server
## Available endpoints
* `/stop`:
* Required parameters:
* `secret_key` - The key specified in the server configuration
* `/event.get`
* The endpoint is not yet ready
* Required parameters:
* `secret_key` - The key specified in the server configuration

View File

@ -0,0 +1,4 @@
# MultiLanguage - Поддержка i18n
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json)\
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.

View File

@ -0,0 +1,37 @@
import json
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
cfg_file = "config.json"
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# Инициализация плагина
with open(cfg_file, 'w') as f:
json.dump(config, f)
cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
async def start():
# Запуск процессов плагина
await ev.call_async_event("my_event")
await ev.call_async_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
async def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")

View File

@ -1,36 +1,37 @@
import KuiToi # Import server object
import json
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
log = beam.log # Use logger from server
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
cfg_file = "config.json"
def on_load():
# When plugin initialization Server uses plugin.load() to load plugin.
# def load(): is really needed
log.info(beam.name)
def my_event_handler(event_data):
log.info(f"{event_data}")
# Events handlers
def on_started():
# Simple event handler
log.info("Server starting...")
def load():
# Инициализация плагина
with open(cfg_file, 'w') as f:
json.dump(config, f)
cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
# Simple event register
beam.register_event("on_started", on_started)
def start():
# Запуск процессов плагина
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
def any_func(data=None):
# Custom event handler
log.info(f"Data from any_func: {data}")
# Create custom event
beam.register_event("my_event", any_func)
# Call custom event
beam.call_event("my_event")
beam.call_event("my_event", "Some data")
# This will be an error since any_func accepts only one argument at the input
beam.call_event("my_event", "Some data", "Some data1")
def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")

View File

@ -12,24 +12,81 @@
## Пример
```python
import KuiToi
try:
import KuiToi
except ImportError:
pass
beam = KuiToi("TestPlugin")
logger = beam.log
kt = KuiToi("Example")
log = kt.log
def load(): # Plugins load from here
print(beam.name)
def my_event_handler(event_data):
log.info(f"{event_data}")
def on_started():
logger.info("Server starting...")
def load():
# Инициализация плагина
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
beam.register_event("on_started", on_started)
def start():
# Запуск процессов плагина
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")
```
Так же более обширный пример можно найти в [example.py](./example.py)
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
* Создание своего ивента : `kt.register_event("my_event", my_event_function)` -
* Вызов ивента: `kt.call_event("my_event")`
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
* Базовые ивенты: _Позже напишу_
* Базовые ивенты: ['on_started', 'on_auth, 'on_stop']
* Создание своего ивента : `beam.register_event("my_event", my_event_function)`
* Вызов ивента: `beam.call_event("my_event")`
* Вызов ивента с данными: `beam.call_event("my_event", data, data2)`
* Вызовы с указанием переменой _**не поддерживаются**_: `beam.call_event("my_event", data=data)`
## Async функции
Поддержка async есть
```python
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# Инициализация плагина
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
async def start():
# Запуск процессов плагина
await ev.call_async_event("my_event")
await ev.call_async_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
async def unload():
# Код завершающий все процессы
log.info("Плагин выгружен успешно.")
```
Так же более обширный пример можно найти в [async_example.py](./async_example.py)
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию)
* Вызов async ивента: `kt.call_async_event("my_event")`
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
* Базовые async ивенты: _Позже напишу_

View File

@ -5,5 +5,5 @@
1. Настройка и Запуск сервера - [тута](./setup)
2. Плагины и Ивент система - [тута](./plugins)
3. Мультиязычность - [тута](./multilanguage)
4. RESP API - [тута](./web)
4. KuiToi WebAPI - [тута](./web)
5. Тут будет что-то ново....

View File

@ -38,12 +38,39 @@ Game:
players: 8
Server:
debug: false
description: This server uses KuiToi!
description: Welcome to KuiToi Server!
language: en
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
server_port: 30813
WebAPI:
enabled: false
secret_key: <random_key>
server_ip: 127.0.0.1
server_port: 8433
```
### Auth
* Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
* Введя BeamMP ключ сервер появится в списке лаунчера.
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys)
### Game
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
* `max_cars` - Максимальное количество машин на игрока
* `players` - Максимально количество игроков
### Server
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
* `description` - Описания сервера для лаунчера BeamMP
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
* `name` - Названия сервер для лаунчера BeamMP
* `server_ip` - Какой IP адрес занять серверу (только для опытных пользователей, по умолчанию 0.0.0.0)
* `server_port` - На каком порту будет работать сервер
### WebAPI
##### _Доки не готовы_

View File

@ -1,3 +1,14 @@
# Web RESP API для сервера
# WebAPI для сервера
## Доступные endpoints
* `/stop`:
* Необходимые парамеры:
* `secret_key` - Ключ, который указан в конфигурации сервера
* `/event.get`
* Точка не готова
* Необходимые парамеры:
* `secret_key` - Ключ, который указан в конфигурации сервера
В разработке

View File

@ -8,48 +8,85 @@ from core import utils
class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
self.down_rw = (None, None)
self.log = utils.get_logger("client(None:0)")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid = -1
self.key = None
self.nick = None
self.roles = None
self.guest = True
self.alive = True
self.ready = False
self.__reader = reader
self.__writer = writer
self._down_rw = (None, None)
self.__Core = core
self.__alive = True
self._loop = asyncio.get_event_loop()
self._log = utils.get_logger("client(None:0)")
self._addr = writer.get_extra_info("sockname")
self._cid = -1
self._key = None
self._nick = None
self._roles = None
self._guest = True
self._ready = False
self._cars = []
@property
def log(self):
return self._log
@property
def addr(self):
return self._addr
@property
def cid(self):
return self._cid
@property
def key(self):
return self._key
@property
def nick(self):
return self._nick
@property
def roles(self):
return self._roles
@property
def guest(self):
return self._guest
@property
def ready(self):
return self._ready
@property
def cars(self):
return self.cars
def _update_logger(self):
self.log = utils.get_logger(f"{self.nick}:{self.cid})")
self._log = utils.get_logger(f"{self.nick}:{self.cid}")
self.log.debug(f"Update logger")
def is_disconnected(self):
if not self.alive:
if not self.__alive:
return True
res = self.writer.is_closing()
res = self.__writer.is_closing()
if res:
self.log.debug(f"Disconnected.")
self.alive = False
self.__alive = False
return True
else:
self.log.debug(f"Alive.")
self.alive = True
self.__alive = True
return False
async def kick(self, reason):
if not self.alive:
if not self.__alive:
self.log.debug(f"Kick({reason}) skipped;")
return
# TODO: i18n
self.log.info(f"Kicked with reason: \"{reason}\"")
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
self.alive = False
await self._tcp_send(b"K" + bytes(reason, "utf-8"))
self.__alive = False
async def tcp_send(self, data, to_all=False, writer=None):
async def _tcp_send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
@ -58,17 +95,24 @@ class Client:
# size data
if writer is None:
writer = self.writer
writer = self.__writer
if to_all:
for client in self.Core.clients:
if not client:
code = data[:1]
for client in self.__Core.clients:
if not client or (client == self and not to_self):
continue
await client.tcp_send(data)
if not to_udp or code in [b'W', b'Y', b'V', b'E']:
if code in [b'O', b'T'] or len(data) > 1000:
# TODO: Compress data
await client._tcp_send(data)
else:
await client._tcp_send(data)
else:
# TODO: UDP send
pass
return
if len(data) == 10:
data += b"."
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len: {len(data)}; send: {header + data!r}')
try:
@ -76,11 +120,11 @@ class Client:
await writer.drain()
except ConnectionError:
self.log.debug('tcp_send: Disconnected')
self.alive = False
self.__alive = False
async def recv(self):
async def _recv(self):
try:
header = await self.reader.read(4)
header = await self.__reader.read(4)
int_header = 0
for i in range(len(header)):
@ -89,7 +133,7 @@ class Client:
if int_header <= 0:
await asyncio.sleep(0.1)
self.is_disconnected()
if self.alive:
if self.__alive:
self.log.debug(f"Header: {header}")
await self.kick("Invalid packet - header negative")
return b""
@ -100,7 +144,7 @@ class Client:
f"assuming malicious intent and disconnecting the client.")
return b""
data = await self.reader.read(100 * MB)
data = await self.__reader.read(100 * MB)
self.log.debug(f"header: `{header}`; int_header: `{int_header}`; data: `{data}`;")
if len(data) != int_header:
@ -113,13 +157,13 @@ class Client:
return data
return data
except ConnectionError:
self.alive = False
self.__alive = False
return b""
# TODO: Speed limiter
async def _split_load(self, start, end, d_sock, filename):
# TODO: Speed limiter
real_size = end - start
writer = self.down_rw[1] if d_sock else self.writer
writer = self._down_rw[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv'
if config.Server["debug"]:
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
@ -132,20 +176,20 @@ class Client:
await writer.drain()
self.log.debug(f"[{who}] File sent.")
except ConnectionError:
self.alive = False
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
return real_size
async def sync_resources(self):
while self.alive:
data = await self.recv()
async def _sync_resources(self):
while self.__alive:
data = await self._recv()
self.log.debug(f"data: {data!r}")
if data.startswith(b"f"):
file = data[1:].decode("utf-8")
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
size = -1
for mod in self.Core.mods_list:
for mod in self.__Core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
@ -154,12 +198,12 @@ class Client:
break
self.log.debug(f"Mode size: {size}")
if size == -1:
await self.tcp_send(b"CO")
await self._tcp_send(b"CO")
await self.kick(f"Not allowed mod: " + file)
return
await self.tcp_send(b"AG")
await self._tcp_send(b"AG")
t = 0
while not self.down_rw[0]:
while not self._down_rw[0]:
await asyncio.sleep(0.1)
t += 1
if t > 50:
@ -177,14 +221,14 @@ class Client:
lost = size - sent
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
if not ok:
self.alive = False
self.__alive = False
# TODO: i18n
self.log.error(f"Error while sending.")
return
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.Core.mods_list:
for mod in self.__Core.mods_list:
if type(mod) == int:
continue
path_list += f"{mod['path']};"
@ -192,55 +236,99 @@ class Client:
mod_list = path_list + size_list
self.log.debug(f"Mods List: {mod_list}")
if len(mod_list) == 0:
await self.tcp_send(b"-")
await self._tcp_send(b"-")
else:
await self.tcp_send(bytes(mod_list, "utf-8"))
await self._tcp_send(bytes(mod_list, "utf-8"))
elif data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
await self._tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
return
async def looper(self):
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
async def _looper(self):
await self._tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
await self._sync_resources()
# TODO: GlobalParser
while self.__alive:
data = await self._recv()
if not data:
self.__alive = False
break
else:
await asyncio.sleep(.1)
self.is_disconnected()
continue
if 89 >= data[0] >= 86:
# TODO: Network.SendToAll
pass
code = data.decode()[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
self.ready = True
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
self._ready = True
ev.call_event("player_join", self)
await ev.call_async_event("player_join", self)
bnick = bytes(self.nick, "utf-8")
await self._tcp_send(b"Sn" + bnick, to_all=True) # I don't know for what it
await self._tcp_send(b"JWelcome" + bnick + b"!", to_all=True) # Hello message
# TODO: Sync cars
# for client in self.__Core.clients:
# for car in client.cars:
# await self._tcp_send(car)
case "C":
# Chat
ev.call_event("chat_receive", f"{data}")
await self.tcp_send(data, to_all=True)
msg = data[2:].decode()
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
continue
self.log.info(f"Received message: {msg}")
# TODO: Handle chat event
ev_data = ev.call_event("chat_receive", msg)
d2 = await ev.call_async_event("chat_receive", msg)
ev_data.extend(d2)
self.log.info(f"TODO: Handle chat event; {ev_data}")
await self._tcp_send(data, to_all=True)
async def remove_me(self):
case "O":
# TODO: ParseVehicle
pass
case "E":
# TODO: HandleEvent
pass
case "N":
# TODO: N
pass
case _:
pass
async def _remove_me(self):
await asyncio.sleep(0.3)
self.alive = False
self.__alive = False
if (self.cid > 0 or self.nick is not None) and \
self.Core.clients_by_nick.get(self.nick):
self.__Core.clients_by_nick.get(self.nick):
# if self.ready:
# await self.tcp_send(b"", to_all=True) # I'm disconnected.
self.log.debug(f"Removing client {self.nick}:{self.cid}")
# TODO: i18n
self.log.info("Disconnected")
self.Core.clients[self.cid] = None
self.Core.clients_by_id.pop(self.cid)
self.Core.clients_by_nick.pop(self.nick)
self.__Core.clients[self.cid] = None
self.__Core.clients_by_id.pop(self.cid)
self.__Core.clients_by_nick.pop(self.nick)
else:
self.log.debug(f"Removing client; Closing connection...")
if not self.writer.is_closing():
self.writer.close()
_, down_w = self.down_rw
try:
if not self.__writer.is_closing():
self.__writer.close()
except Exception as e:
self.log.debug(f"Error while closing writer: {e}")
try:
_, down_w = self._down_rw
if down_w and not down_w.is_closing():
down_w.close()
except Exception as e:
self.log.debug(f"Error while closing download writer: {e}")

View File

@ -8,26 +8,50 @@ from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self.log = utils.get_logger("client(id: )")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid: int = -1
self.key: str = None
self.nick: str = None
self.roles: str = None
self.guest = True
self.alive = True
self.ready = False
self.__reader = reader
self.__writer = writer
self._down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self._log = utils.get_logger("client(id: )")
self._addr = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self.__Core = core
self._cid: int = -1
self._key: str = None
self._nick: str = None
self._roles: str = None
self._guest = True
self.__alive = True
self._ready = False
@property
def log(self):
return self._log
@property
def addr(self):
return self._addr
@property
def cid(self):
return self._cid
@property
def key(self):
return self._key
@property
def nick(self):
return self._nick
@property
def roles(self):
return self._roles
@property
def guest(self):
return self._guest
@property
def ready(self):
return self._ready
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def _tcp_send(self, data: bytes, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
async def _sync_resources(self) -> None: ...
async def _recv(self) -> bytes: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> None: ...
async def looper(self) -> None: ...
async def _looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def remove_me(self) -> None: ...
async def _remove_me(self) -> None: ...

View File

@ -43,7 +43,11 @@ class Core:
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
def get_client(self, cid=None, nick=None):
ev.register_event("get_player", self.get_client)
def get_client(self, cid=None, nick=None, from_ev=None):
if from_ev is not None:
return self.get_client(*from_ev['args'], **from_ev['kwargs'])
if cid is not None:
return self.clients_by_id.get(cid)
if nick:
@ -61,7 +65,7 @@ class Core:
break
await asyncio.sleep(random.randint(3, 9) * 0.01)
if not self.clients[cid]:
client.cid = cid
client._cid = cid
self.clients_by_nick.update({client.nick: client})
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
self.clients_by_id.update({client.cid: client})
@ -81,7 +85,7 @@ class Core:
for client in self.clients:
if not client:
continue
out += f"{client.nick}"
out += f"{client._nick}"
if need_cid:
out += f":{client.cid}"
out += ","
@ -100,7 +104,7 @@ class Core:
if not client.ready:
client.is_disconnected()
continue
await client.tcp_send(bytes(ca, "utf-8"))
await client._tcp_send(bytes(ca, "utf-8"))
@staticmethod
def start_web():
@ -252,30 +256,33 @@ class Core:
tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
ev.call_event("_plugins_start")
await ev.call_async_event("_plugins_start")
self.run = True
self.log.info(i18n.start)
ev.call_event("server_started")
await ev.call_async_event("server_started")
await t # Wait end.
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Exception: {e}")
self.log.exception(e)
except KeyboardInterrupt:
pass
finally:
self.run = False
self.tcp.stop()
# self.udp.stop()
self.run = False
await self.stop()
def start(self):
asyncio.run(self.main())
def stop(self):
async def stop(self):
ev.call_event("server_stopped")
ev.call_event("_plugins_unload")
await ev.call_async_event("server_stopped")
await ev.call_async_event("_plugins_unload")
self.run = False
self.log.info(i18n.stop)
if config.WebAPI["enabled"]:
asyncio.run(self.web_stop())
exit(0)
# exit(0)

View File

@ -45,4 +45,4 @@ class Core:
async def heartbeat(self, test=False) -> None: ...
async def main(self) -> None: ...
def start(self) -> None: ...
def stop(self) -> None: ...
async def stop(self) -> None: ...

View File

@ -25,36 +25,36 @@ class TCPServer:
client = self.Core.create_client(reader, writer)
# TODO: i18n
self.log.info(f"Identifying new ClientConnection...")
data = await client.recv()
data = await client._recv()
self.log.debug(f"Version: {data}")
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
# TODO: i18n
await client.kick("Outdated Version.")
return False, client
else:
await client.tcp_send(b"S") # Accepted client version
await client._tcp_send(b"S") # Accepted client version
data = await client.recv()
data = await client._recv()
self.log.debug(f"Key: {data}")
if len(data) > 50:
# TODO: i18n
await client.kick("Invalid Key (too long)!")
return False, client
client.key = data.decode("utf-8")
client._key = data.decode("utf-8")
ev.call_event("auth_sent_key", client)
try:
async with aiohttp.ClientSession() as session:
url = 'https://auth.beammp.com/pkToUser'
async with session.post(url, data={'key': client.key}) as response:
async with session.post(url, data={'key': client._key}) as response:
res = await response.json()
self.log.debug(f"res: {res}")
if res.get("error"):
# TODO: i18n
await client.kick('Invalid key! Please restart your game.')
return False, client
client.nick = res["username"]
client.roles = res["roles"]
client.guest = res["guest"]
client._nick = res["username"]
client._roles = res["roles"]
client._guest = res["guest"]
# noinspection PyProtectedMember
client._update_logger()
except Exception as e:
@ -89,7 +89,7 @@ class TCPServer:
cid = (await reader.read(1))[0]
client = self.Core.get_client(cid=cid)
if client:
client.down_rw = (reader, writer)
client._down_rw = (reader, writer)
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
else:
writer.close()
@ -102,7 +102,7 @@ class TCPServer:
case "C":
result, client = await self.auth_client(reader, writer)
if result:
await client.looper()
await client._looper()
return result, client
case "D":
await self.set_down_rw(reader, writer)
@ -128,7 +128,7 @@ class TCPServer:
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
_, cl = await self.handle_code(code, reader, writer)
if cl:
await cl.remove_me()
await cl._remove_me()
del cl
break
except Exception as e:
@ -152,7 +152,9 @@ class TCPServer:
# TODO: i18n
self.log.error("Cannot bind port")
raise e
except BaseException as e:
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Error: {e}")
raise e
finally:

View File

@ -14,17 +14,13 @@ parser.add_argument('-v', '--version', action="store_true", help='Print version
parser.add_argument('--config', help='Patch to config file.', nargs='?', default=None, type=str)
parser.add_argument('--language', help='Setting localisation.', nargs='?', default=None, type=str)
run = True
def main():
from core import Core
core = Core()
try:
core.start()
Core().start()
except KeyboardInterrupt:
core.run = False
core.stop()
pass
if __name__ == '__main__':

View File

@ -13,51 +13,80 @@ class EventsSystem:
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"server_started": [],
"_plugins_start": [],
"auth_sent_key": [],
"auth_ok": [],
"auth_sent_key": [], # Only sync
"auth_ok": [], # Only sync
"player_join": [],
"chat_receive": [],
"_plugins_unload": [],
"server_stopped": [],
}
self.__async_events = {
"server_started": [],
"_plugins_start": [],
"_plugins_unload": [],
"player_join": [],
"chat_receive": [],
"server_stopped": []
}
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def register_event(self, event_name, event_func):
def register_event(self, event_name, event_func, async_event=False):
self.log.debug(f"register_event({event_name}, {event_func}):")
if not callable(event_func):
# TODO: i18n
self.log.error(f"Cannot add event '{event_name}'. "
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return
if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events:
self.__async_events.update({str(event_name): [event_func]})
else:
self.__async_events[event_name].append(event_func)
else:
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Using event '{event_name}'")
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
if inspect.iscoroutinefunction(func):
d = self.loop.run_until_complete(func(event_data))
else:
d = func(event_data)
funcs_data.append(d)
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
# TODO: i18n
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
self.log.exception(e)
else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_event()?. Just skipping it...")
return funcs_data
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
# TODO: i18n
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
self.log.exception(e)
else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
return funcs_data

View File

@ -1,6 +1,11 @@
from typing import Any
class EventsSystem:
@staticmethod
def register_event(event_name, event_func): ...
@staticmethod
def call_event(event_name, *data, **kwargs): ...
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
@staticmethod
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
class ev(EventsSystem): ...

View File

@ -1,15 +1,15 @@
{
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"hello": "Greetings from KuiToi Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization complete.",
"start": "Server started!",
"stop": "Goodbye!",
"stop": "Server stopped!",
"": "Server auth",
"auth_need_key": "BEAM key needed for starting the server!",
"auth_empty_key": "Key is empty!",
"auth_cannot_open_browser": "Cannot open browser: {}",
"auth_need_key": "A BeamMP key is required to start the server!",
"auth_empty_key": "The BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"": "GUI phases",
@ -17,32 +17,32 @@
"GUI_no": "No",
"GUI_ok": "Ok",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
"GUI_enter_key_message": "Please type your key:",
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
"": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.",
"man_for": "Manual for command",
"man_message_not_found": "man: Manual message not found.",
"man_command_not_found": "man: command \"{}\" not found!",
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Displays help page for COMMAND.",
"man_for": "Help page for",
"man_message_not_found": "man: Help page not found.",
"man_command_not_found": "man: Command \"{}\" not found!",
"": "Command: help",
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
"help_message_help": "Display names and brief descriptions of available commands",
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
"help_message_help": "Displays the names and short descriptions of commands.",
"help_command": "Command",
"help_message": "Help message",
"help_message_not_found": "No help message found",
"help_message": "Description",
"help_message_not_found": "No description available.",
"": "Command: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
"help_message_stop": "Server shutdown.",
"man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
"help_message_exit": "Server shutdown."
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}

View File

@ -90,16 +90,16 @@ class MultiLanguage:
# noinspection PyDictDuplicateKeys
self.__data = {
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"hello": "Greetings from KuiToi Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization complete.",
"start": "Server started!",
"stop": "Goodbye!",
"stop": "Server stopped!",
"": "Server auth",
"auth_need_key": "BEAM key needed for starting the server!",
"auth_empty_key": "Key is empty!",
"auth_cannot_open_browser": "Cannot open browser: {}",
"auth_need_key": "A BeamMP key is required to start the server!",
"auth_empty_key": "The BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"": "GUI phases",
@ -107,35 +107,35 @@ class MultiLanguage:
"GUI_no": "No",
"GUI_ok": "Ok",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
"GUI_enter_key_message": "Please type your key:",
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
"": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.",
"man_for": "Manual for command",
"man_message_not_found": "man: Manual message not found.",
"man_command_not_found": "man: command \"{}\" not found!",
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Displays help page for COMMAND.",
"man_for": "Help page for",
"man_message_not_found": "man: Help page not found.",
"man_command_not_found": "man: Command \"{}\" not found!",
"": "Command: help",
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
"help_message_help": "Display names and brief descriptions of available commands",
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
"help_message_help": "Displays the names and short descriptions of commands.",
"help_command": "Command",
"help_message": "Help message",
"help_message_not_found": "No help message found",
"help_message": "Description",
"help_message_not_found": "No description available.",
"": "Command: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
"help_message_stop": "Server shutdown.",
"man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
"help_message_exit": "Server shutdown."
}
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}
self.__i18n = i18n(self.__data)
def open_file(self):