96 Commits

Author SHA1 Message Date
8139cbf8bc 0.4.0 -> 0.4.1 2023-07-20 04:10:51 +03:00
eec7c8129d Add snowman support (Only one per player);
Optimize getting car_id;
Minor fixes;
2023-07-20 04:07:39 +03:00
158599dfc5 Debug t and m codes 2023-07-20 02:22:20 +03:00
06bd50f0fa Add UDP events: onChangePosition, onSentPing 2023-07-20 01:59:30 +03:00
e086fea2e9 Update __build__; 2023-07-20 01:56:36 +03:00
b6038ee6d0 Move code from case to func;
Add events for cars: onCarDelete, onCarReset;
Minor updates;
2023-07-20 01:54:52 +03:00
147e76e089 Remove some debug info;
Optimize code.
2023-07-20 01:38:56 +03:00
56b9049dcb BeamMP_version 2023-07-20 01:37:52 +03:00
78d323644d Remove some debug data! 2023-07-20 01:34:39 +03:00
310c47162c FIX config.enc 2023-07-20 01:27:01 +03:00
27d49cf5cc Update TODOs 2023-07-20 01:15:32 +03:00
a5a7a5dfc9 Add events onCarSpawn, onCarEdited 2023-07-19 22:59:30 +03:00
f6ff018b03 connect_time 2023-07-19 21:53:08 +03:00
1829113ae5 Add upload use_queue 2023-07-19 21:22:29 +03:00
e72c371e20 Minor updates 2023-07-19 21:10:26 +03:00
57b7cebeca Minor updates 2023-07-19 21:10:04 +03:00
2a2d55946e Update config_provider 2023-07-19 21:04:05 +03:00
ea2d715cae DO Speed limiter;
Minor fixes;
2023-07-19 20:23:16 +03:00
102891c8e8 0.3.0 -> 0.4.0 2023-07-19 03:53:27 +03:00
46b0419340 Update TODOs 2023-07-19 03:50:33 +03:00
47cca3a0d8 Add UDP send;
Remove rights to spawn unicycle (For now);
Minor updates;
2023-07-19 03:48:57 +03:00
77ee76c0c0 UDP Part ready! 2023-07-19 03:45:02 +03:00
852e977a75 Fix set udp sock 2023-07-18 22:59:12 +03:00
407127ec97 Typing update 2023-07-18 22:58:34 +03:00
7dd3faac12 Update TODOs 2023-07-18 22:43:49 +03:00
ef69df10d6 Compress DATA 2023-07-18 22:40:35 +03:00
a226b17612 Compress DATA 2023-07-18 22:40:27 +03:00
69348e9339 Some fixes in onChatReceive 2023-07-18 22:33:49 +03:00
31d8cf7842 try to check_alive 2023-07-18 22:33:15 +03:00
45d45a820c Update TODOs 2023-07-18 22:31:41 +03:00
aa440a1e3d Update TODOs 2023-07-18 22:31:14 +03:00
63c9515e86 Add UDP part 2023-07-18 22:08:00 +03:00
cfeb2e9823 heartbeat fix 2023-07-18 19:53:02 +03:00
85b85114b5 heartbeat fix 2023-07-18 19:52:44 +03:00
792884d7b0 Disable __handle_packet...;
Fix "Invalid packet - header negative";
Fix _get_cid_vid;
Add dta to _handle_vehicle_codes;
Fix disconnected message.
2023-07-18 05:35:34 +03:00
a5b087f8b4 Disable __handle_packet...;
Fix "Invalid packet - header negative";
Fix _get_cid_vid;
Add dta to _handle_vehicle_codes;
Fix disconnected message.
2023-07-18 05:35:17 +03:00
a01567c89a Auto removing cars after disconnect 2023-07-18 04:14:46 +03:00
041883644c UDP... 2023-07-18 04:10:56 +03:00
3d33eec5fd Minor fix 2023-07-18 04:10:42 +03:00
3f2c5b24f9 Add Disconnected message 2023-07-17 22:26:42 +03:00
b7ea7ff362 Beta!!! 2023-07-17 21:28:25 +03:00
07ec15170b 0.2.3 -> 0.3.0 2023-07-17 21:26:28 +03:00
eb88af247c Add reset and edit for cars 2023-07-17 21:23:40 +03:00
6dedf518e2 Update TODOs 2023-07-17 21:22:58 +03:00
69ee180128 Fix 2023-07-17 19:42:56 +03:00
98f86b2248 Minor update 2023-07-17 18:16:07 +03:00
98ef332193 Update TODOs 2023-07-17 17:11:59 +03:00
642c91d59c Packets handled (Recursive finding second packet) 2023-07-17 17:04:25 +03:00
acdb32d900 Update TODOs 2023-07-17 14:25:07 +03:00
50b1e7b176 Recreate cars system;
Move code "O" to client._handle_vehicle_codes();
2023-07-17 14:18:52 +03:00
c9e6a0a9cd Change evens naming semantic 2023-07-16 16:14:16 +03:00
cd098571d9 Change evens naming semantic 2023-07-16 16:14:00 +03:00
a73b14f9b4 Fix car spawning 2023-07-16 10:42:34 +03:00
e3e5c6ecbb Update TODOs 2023-07-16 09:50:02 +03:00
5953923368 DO Sync cars;
DO Create cars;
2023-07-16 09:48:53 +03:00
580b836e39 Minor fixes 2023-07-16 09:37:38 +03:00
4974d48411 0.2.2 -> 0.2.3 2023-07-16 03:07:47 +03:00
3b7842aa50 Update modules version 2023-07-16 03:06:38 +03:00
db6eb361b8 0.2.2 -> 0.2.3 2023-07-16 03:06:22 +03:00
479525a66e Remove nick, roles from protected;
Change _tcp_send -> _send;
Now _send data may be str;
Rewrite to_all handler;
Add chat handler;
2023-07-16 02:55:09 +03:00
6d4bc1e72c Minor update 2023-07-16 02:52:20 +03:00
9b3677de46 Minor update 2023-07-16 02:52:15 +03:00
58137752c5 Add commands plugins, pl 2023-07-16 02:50:25 +03:00
220c6068e4 Minor update 2023-07-16 02:49:58 +03:00
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
5f8b70a2ee Update version 2023-07-15 11:03:05 +03:00
a66f3d8b36 Update TODOs 2023-07-15 11:00:46 +03:00
4c3da30a94 Add new events. 2023-07-15 10:59:50 +03:00
9c52e41b99 Add new logs to class KuiToi;
load:
    Add encoding to open();
    Add plugin attributes error;
    Add plugin.start();
    Add plugin.unload();
    Recreate plugin settings;
    Add async support;
2023-07-15 10:59:33 +03:00
51f960f7c2 Remove print() 2023-07-15 10:53:38 +03:00
0cbed05d68 Move plugins loader to core. 2023-07-15 10:52:58 +03:00
c6c6ec31b0 Add new stock events;
Add async support to call_event();
Add **kwargs t0 call_event();
Sends new data to events;
2023-07-15 10:52:25 +03:00
8feba0e085 Minor update 2023-07-15 09:17:52 +03:00
a5202edf83 Relocate open() in plugins 2023-07-15 02:17:00 +03:00
64ce662d04 Add methods in KuiToi class 2023-07-15 02:15:11 +03:00
08e4f0fcba Update TODOs 2023-07-14 23:11:03 +03:00
99f40eadb0 Add returns from ev.call_events 2023-07-14 22:29:37 +03:00
5a40ab8b05 Add TODOs to code. 2023-07-14 22:27:23 +03:00
4f688d7c02 Update LICENCE 2023-07-14 21:55:19 +03:00
c4fe201b86 Move class Client to self file. 2023-07-14 21:52:24 +03:00
a7a9f367c5 0.2.1 -> 0.2.2 2023-07-14 19:17:21 +03:00
8af4e6527f Update TODOs 2023-07-14 19:14:51 +03:00
7bda3dce29 core.py:
Change default cid to -1 for auth;
    Change client logger;
    Update logic of client.kick();
    Remove "kostil" :)));
    Add client._split_load().....(5h);
    DO SendFile - sending mods.;
    Move sync_resources back;
    Add client.ready for blocking "alive" info;
    Refactor client.remove_me();
    Add name for web Thread;
    Minor fixes;
tcp_server.py:
    From auth always return client;
    Add events call in auth;
    Refactor set_down_rw;
    Minor fixes;
2023-07-14 19:13:13 +03:00
6afe62b68e Move sync_resources logic before insert to clients. 2023-07-13 18:08:47 +03:00
37 changed files with 1740 additions and 637 deletions

View File

@@ -6,6 +6,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,59 +1,72 @@
# KuiToi-Server
## About
**_[Status: Alpha]_** \
**_[Status: Beta]_** \
BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
## TODOs
- [ ] Server core
- [x] Server core
- [x] BeamMP System
- [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher)
- [X] Player authentication
- [x] TCP Server part:
- [x] Handle code
- [x] Understanding BeamMP header
- [x] Upload mods
- [x] Connecting to the world
- [x] Chat
- [x] Players online counter
- [x] Packets handled (Recursive finding second packet)
- [ ] Client events
- [x] Car synchronizations:
- [x] State packets
- [x] Spawn cars
- [x] Delete cars
- [x] Edit cars
- [x] Reset cars
- [x] "ABG:" (compressed data)
- [x] Decompress data
- [x] Compress data
- [x] UDP Server part:
- [x] Ping
- [x] Position synchronizations
- [x] Additional:
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] TCP Server part:
- [x] Handle code
- [x] Understanding BeamMP header
- [ ] Upload mods
- [x] Connecting to the world
- [x] Chat
- [x] Players online counter
- [ ] Car state synchronizations _(Codes: We, Vi)_
- [ ] "ABG:" (compressed data)
- [x] Decompress data
- [ ] Vehicle data _(Code: Os)_
- [ ] UDP Server part:
- [ ] Players synchronizations _(Code: Zp)_
- [ ] Ping _(Code: p)_
- [x] Additional:
- [x] Logger
- [x] Just logging
- [x] Log in file
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [ ] _(Deferred)_ Static text (bug)
- [x] History
- [x] Autocomplete
- [x] Events System
- [x] Call events
- [x] Create custom events
- [ ] Return from events
- [x] Return from events
- [x] Async support
- [ ] Add all events
- [x] Plugins support
- [ ] KuiToi class
- [ ] Client (Player) class
- [x] Load Python plugins
- [x] Async support
- [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support)
- [x] Core
- [ ] Core
- [x] Console
- [x] WebAPI
- [x] HTTP API Server (fastapi)
- [ ] HTTP API Server (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] [Documentation](docs/en/readme.md)
- [ ] [Documentation](./docs/)
## Installation
@@ -78,6 +91,6 @@ Copyright (c) 2023 SantaSpeen (Maxim Khomutov)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

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` - Ключ, который указан в конфигурации сервера
В разработке

627
src/core/Client.py Normal file
View File

@@ -0,0 +1,627 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import json
import math
import time
import zlib
from core import utils
class Client:
def __init__(self, reader, writer, core):
self.__reader = reader
self.__writer = writer
self.__Core = core
self.__alive = True
self.__packets_queue = []
self.__tasks = []
self._down_sock = (None, None)
self._udp_sock = (None, None)
self._loop = asyncio.get_event_loop()
self._log = utils.get_logger("player(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 = [None] * 21 # Max 20 cars per player + 1 snowman
self._snowman = {"id": -1, "packet": ""}
self._connect_time = 0
@property
def _writer(self):
return self.__writer
@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 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.debug(f"Update logger")
def is_disconnected(self):
if not self.__alive:
return True
if self.__writer.is_closing():
self.log.debug(f"is_d: Disconnected.")
self.__alive = False
return True
else:
self.__alive = True
return False
async def kick(self, reason):
if not self.__alive:
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
return
# TODO: i18n
self.log.info(f"Kicked with reason: \"{reason}\"")
await self._send(f"K{reason}")
self.__alive = False
async def send_message(self, message, to_all=True):
pass
async def send_event(self, event_name, event_data):
pass
async def _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.
# [][][][][][]...[]
# ^------^^---...-^
# size data
if type(data) == str:
data = bytes(data, config.enc)
if to_all:
code = chr(data[0])
for client in self.__Core.clients:
if not client or (client is self and not to_self):
continue
if not to_udp or code in ['V', 'W', 'Y', 'E']:
await client._send(data)
else:
await client._send(data, to_udp=to_udp)
return
if not self.__alive:
return False
if writer is None:
writer = self.__writer
if len(data) > 400:
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
if to_udp:
udp_sock = self._udp_sock[0]
udp_addr = self._udp_sock[1]
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
if udp_sock and udp_addr:
try:
if not udp_sock.is_closing():
# self.log.debug(f'[UDP] {data!r}')
udp_sock.sendto(data, udp_addr)
except OSError:
self.log.debug("[UDP] Error sending")
except Exception as e:
self.log.debug(f"[UDP] Error sending: {e}")
self.log.exception(e)
return
header = len(data).to_bytes(4, "little", signed=True)
# self.log.debug(f'[TCP] {header + data!r}')
try:
writer.write(header + data)
await writer.drain()
return True
except ConnectionError:
self.log.debug('[TCP] Disconnected')
self.__alive = False
await self._remove_me()
return False
async def _recv(self, one=False):
while self.__alive:
try:
header = await self.__reader.read(4)
int_header = int.from_bytes(header, byteorder='little', signed=True)
if int_header <= 0:
await asyncio.sleep(0.1)
self.is_disconnected()
if self.__alive:
if header == b"":
self.__packets_queue.append(None)
self.__alive = False
continue
self.log.error(f"Header: {header}")
await self.kick("Invalid packet - header negative")
self.__packets_queue.append(None)
continue
if int_header > 100 * MB:
await self.kick("Header size limit exceeded")
self.log.warning("Client sent header of >100MB - "
"assuming malicious intent and disconnecting the client.")
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
self.__packets_queue.append(None)
continue
data = await self.__reader.read(int_header)
# self.log.debug(f"int_header: {int_header}; data: `{data}`;")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
# self.log.debug(f"ABG Packet: {len(data)}")
if one:
return data
self.__packets_queue.append(data)
except ConnectionError:
self.__alive = False
self.__packets_queue.append(None)
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
real_size = end - start
writer = self._down_sock[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv'
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
with open(filename, 'rb') as f:
f.seek(start)
total_sent = 0
start_time = time.monotonic()
while total_sent < real_size:
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
try:
writer.write(data)
await writer.drain()
self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except ConnectionError:
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
break
total_sent += len(data)
# Calculate delay based on speed limit
if speed_limit:
elapsed_time = time.monotonic() - start_time
expected_time = total_sent / (speed_limit * MB)
if expected_time > elapsed_time:
await asyncio.sleep(expected_time - elapsed_time)
return total_sent
async def _sync_resources(self):
while self.__alive:
data = await self._recv(True)
if data.startswith(b"f"):
file = data[1:].decode(config.enc)
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
size = -1
for mod in self.__Core.mods_list:
if type(mod) == int:
continue
if mod.get('path') == file:
size = mod['size']
self.log.debug("File is accept.")
break
self.log.debug(f"Mode size: {size}")
if size == -1:
await self._send(b"CO")
await self.kick(f"Not allowed mod: " + file)
return
await self._send(b"AG")
t = 0
while not self._down_sock[0]:
await asyncio.sleep(0.1)
t += 1
if t > 50:
await self.kick("Missing download socket")
return
if config.Options['use_queue']:
while self.__Core.lock_upload:
await asyncio.sleep(.2)
self.__Core.lock_upload = True
speed = config.Options["speed_limit"]
if speed:
speed = speed / 2
half_size = math.floor(size / 2)
t = time.monotonic()
uploads = [
self._split_load(0, half_size, False, file, speed),
self._split_load(half_size, size, True, file, speed)
]
sl0, sl1 = await asyncio.gather(*uploads)
tr = time.monotonic() - t
if self.__Core.lock_upload:
self.__Core.lock_upload = False
# TODO: i18n
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
if speed:
msg += f" of limit {int(speed * 2)}Mb/s"
self.log.info(msg)
sent = sl0 + sl1
ok = sent == size
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
# TODO: i18n
self.log.error(f"Error while sending: {file!r}")
return
elif data.startswith(b"SR"):
path_list = ''
size_list = ''
for mod in self.__Core.mods_list:
if type(mod) == int:
continue
path_list += f"{mod['path']};"
size_list += f"{mod['size']};"
mod_list = path_list + size_list
self.log.debug(f"Mods List: {mod_list}")
if len(mod_list) == 0:
await self._send(b"-")
else:
await self._send(mod_list)
elif data == b"Done":
await self._send(f"M/levels/{config.Game['map']}/info.json")
break
return
def _get_cid_vid(self, data: str):
sep = data.find(":", 1) + 1
s = data[sep:sep + 3]
id_sep = s.find('-')
if id_sep == -1:
self.log.debug(
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'")
return -1, -1
cid = s[:id_sep]
vid = s[id_sep + 1:]
if cid.isdigit() and vid.isdigit():
try:
cid = int(cid)
vid = int(vid)
return cid, vid
except ValueError:
self.log.debug(f"Invalid packet: Could not parse cid/vid from packet, as one or both are not valid "
f"numbers: '{s}'")
return -1, -1
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'")
return -1, -1
async def _spawn_car(self, data):
car_data = data[2:]
car_id = next((i for i, car in enumerate(self.cars) if car is None), len(self.cars))
cars_count = len(self.cars) - self.cars.count(None)
if self._snowman['id'] != -1:
cars_count -= 1 # -1 for unicycle
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
car_json = {}
try:
car_json = json.loads(car_data[car_data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
allow = True
allow_snowman = True
over_spawn = False
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarSpawn
pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
snowman = car_json.get("jbm") == "unicycle"
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
if snowman:
unicycle_id = self._snowman['id']
if unicycle_id != -1:
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
self._cars[unicycle_id] = None
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._snowman = {"id": car_id, "packet": pkt}
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
else:
self.log.debug(f"Car spawn accepted: car_id={car_id}")
self._cars[car_id] = {
"packet": pkt,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn
}
await self._send(pkt, to_all=True, to_self=True)
else:
await self._send(pkt)
des = f"Od:{self.cid}-{car_id}"
await self._send(des)
async def _delete_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self.cars[car_id]:
admin_allow = False # Delete from admin, for example...
ev_data_list = ev.call_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarDelete
pass
if cid == self.cid or admin_allow:
await self._send(raw_data, to_all=True, to_self=True)
car = self.cars[car_id]
if car['snowman']:
self.log.debug(f"Snowman found")
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
self._cars[unicycle_id] = None
self._cars[car_id] = None
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
self.log.debug(f"Deleted car: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _edit_car(self, raw_data, data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and self.cars[car_id]:
client = self.__Core.get_client(cid=cid)
if client:
car = client.cars[car_id]
new_car_json = {}
try:
new_car_json = json.loads(data[data.find("{"):])
except Exception as e:
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {data}")
allow = False
admin_allow = False
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
ev_data_list.extend(d2)
for ev_data in ev_data_list:
# TODO: handle event onCarEdited
pass
if cid == self.cid or allow or admin_allow:
if car['snowman']:
unicycle_id = self._snowman['id']
self._snowman['id'] = -1
self.log.debug(f"Delete snowman")
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
self._cars[unicycle_id] = None
else:
await self._send(raw_data, to_all=True, to_self=False)
if car['json_ok']:
old_car_json = car['json']
old_car_json.update(new_car_json)
car['json'] = old_car_json
self.log.debug(f"Updated car: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _reset_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self.cars[car_id]:
await self._send(raw_data, to_all=True, to_self=False)
ev.call_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
await ev.call_async_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
self.log.debug(f"Car reset: car_id={car_id}")
else:
self.log.debug(f"Invalid car: car_id={car_id}")
async def _handle_car_codes(self, raw_data):
if len(raw_data) < 6:
return
sub_code = raw_data[1]
data = raw_data[3:]
match sub_code:
case "s": # Spawn car
self.log.debug("Trying to spawn car")
if data[0] == "0":
await self._spawn_car(data)
case "d": # Delete car
self.log.debug("Trying to delete car")
await self._delete_car(raw_data)
case "c": # Edit car
self.log.debug("Trying to edit car")
await self._edit_car(raw_data, data)
case "r": # Reset car
self.log.debug("Trying to reset car")
await self._reset_car(raw_data)
case "t": # Broken details
self.log.debug(f"Something changed/broken: {raw_data}")
await self._send(raw_data, to_all=True, to_self=False)
case "m": # Move focus cat
self.log.debug(f"Move focus to: {raw_data}")
await self._send(raw_data, to_all=True, to_self=True)
async def _connected_handler(self):
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
# Client connected
ev.call_event("onPlayerJoin", player=self)
await ev.call_async_event("onPlayerJoin", player=self)
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
self._ready = True
for client in self.__Core.clients:
if not client:
continue
for car in client.cars:
if not car:
continue
await self._send(car['packet'])
async def _chat_handler(self, data):
sup = data.find(":", 2)
if sup == -1:
await self._send("C:Server: Invalid message.")
msg = data[sup + 2:]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
return
to_ev = {"message": msg, "player": self}
ev_data_list = ev.call_event("onChatReceive", **to_ev)
d2 = await ev.call_async_event("onChatReceive", **to_ev)
ev_data_list.extend(d2)
need_send = True
for ev_data in ev_data_list:
try:
message = ev_data["message"]
to_all = ev_data.get("to_all")
if to_all is None:
to_all = True
to_self = ev_data.get("to_self")
if to_self is None:
to_self = True
to_client = ev_data.get("to_client")
writer = None
if to_client:
# noinspection PyProtectedMember
writer = to_client._writer
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
self.log.info(f"{self.nick}: {msg}")
await self._send(data, to_all=True)
async def _handle_codes(self, data):
if not data:
self.__alive = False
return
# Codes: V W X Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
return
try:
data = data.decode()
except UnicodeDecodeError:
self.log.error(f"UnicodeDecodeError: {data}")
return
# Codes: p, Z in udp_server.py
match data[0]: # At data[0] code
case "H": # Map load, client ready
await self._connected_handler()
case "C": # Chat handler
await self._chat_handler(data)
case "O": # Cars handler
await self._handle_car_codes(data)
case "E": # Client events handler
# TODO: Handle events from client
pass
case "N":
await self._send(data, to_all=True, to_self=False)
async def _looper(self):
self._connect_time = time.monotonic()
await self._send(f"P{self.cid}") # Send clientID
await self._sync_resources()
tasks = self.__tasks
recv = asyncio.create_task(self._recv())
tasks.append(recv)
while self.__alive:
if len(self.__packets_queue) > 0:
for index, packet in enumerate(self.__packets_queue):
# self.log.debug(f"Packet: {packet}")
del self.__packets_queue[index]
task = self._loop.create_task(self._handle_codes(packet))
tasks.append(task)
else:
await asyncio.sleep(0.1)
await asyncio.gather(*tasks)
async def _remove_me(self):
await asyncio.sleep(0.3)
self.__alive = False
if (self.cid > 0 or self.nick is not None) and \
self.__Core.clients_by_nick.get(self.nick):
for i, car in enumerate(self.cars):
if not car:
continue
self.log.debug(f"Removing car: car_id={i}")
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
if self.ready:
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
self.log.debug(f"Removing client")
# TODO: i18n
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.")
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...")
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_sock
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}")

74
src/core/Client.pyi Normal file
View File

@@ -0,0 +1,74 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport
from logging import Logger
from typing import Tuple, List, Dict, Optional, Union
from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self._connect_time: float = 0.0
self.__tasks = []
self.__reader = reader
self.__writer = writer
self.__packets_queue = []
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
self._log = utils.get_logger("client(id: )")
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self.__Core: 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._cars: List[Optional[Dict[str, int]]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
@property
def _writer(self) -> StreamWriter: ...
@property
def log(self) -> Logger: ...
@property
def addr(self) -> Tuple[str, int]: ...
@property
def cid(self) -> int: ...
@property
def key(self) -> str: ...
@property
def guest(self) -> bool: ...
@property
def ready(self) -> bool: ...
@property
def cars(self) -> List[dict | None]: ...
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
async def send_event(self, event_name: str, event_data: str) -> None: ...
async def _send(self, data: bytes | str, 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, one=False) -> bytes | None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _spawn_car(self, data: str) -> None: ...
async def _delete_car(self, raw_data: str) -> None: ...
async def _edit_car(self, raw_data: str, data: str) -> None: ...
async def _reset_car(self, raw_data: str) -> None: ...
async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ...
async def _looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def _remove_me(self) -> None: ...

View File

@@ -2,7 +2,7 @@
# File core.__init__.py
# Written by: SantaSpeen
# Version 1.3
# Core version: 0.2.1
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
@@ -10,8 +10,8 @@
__title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.2.1'
__build__ = 874
__version__ = '0.4.1'
__build__ = 1486 # Я это считаю лог файлами
__author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su'
__license__ = "FPA"
@@ -19,7 +19,6 @@ __copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
import asyncio
import builtins
import os
import webbrowser
import prompt_toolkit.shortcuts as shortcuts
@@ -27,7 +26,7 @@ import prompt_toolkit.shortcuts as shortcuts
from .utils import get_logger
from core.core import Core
from main import parser
from modules import ConfigProvider, EventsSystem, PluginsLoader
from modules import ConfigProvider, EventsSystem
from modules import Console
from modules import MultiLanguage
@@ -47,17 +46,17 @@ if args.config:
config_provider = ConfigProvider(config_path)
config = config_provider.open_config()
builtins.config = config
if config.Server['debug'] is True:
config.enc = config.Options['encoding']
if config.Options['debug'] is True:
utils.set_debug_status()
log.info("Debug enabled!")
log = get_logger("core.init")
log.debug("Debug mode enabled!")
log.debug(f"Server config: {config}")
# i18n init
log.debug("Initializing i18n...")
ml = MultiLanguage()
ml.set_language(args.language or config.Server['language'])
ml.set_language(args.language or config.Options['language'])
ml.builtins_hook()
log.debug("Initializing EventsSystem...")
@@ -107,16 +106,8 @@ console.builtins_hook()
console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop)
console.add_command("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit)
log.debug("Initializing PluginsLoader...")
if not os.path.exists("plugins"):
os.mkdir("plugins")
pl = PluginsLoader("plugins")
pl.load_plugins()
builtins.B = 1
builtins.KB = B * 1024
builtins.MB = KB * 1024
builtins.GB = MB * 1024
builtins.TB = GB * 1024
log.info(i18n.init_ok)

View File

@@ -1,205 +1,26 @@
# Developed by KuiToi Dev
# File core.core.py
# Written by: SantaSpeen
# Core version: 0.2.1
# Version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import os
import random
import zlib
from threading import Thread
import aiohttp
import uvicorn
from core import utils
from core.Client import Client
from core.tcp_server import TCPServer
from core.udp_server import UDPServer
from modules import PluginsLoader
from modules.WebAPISystem import app as webapp
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 = 0
self.key = None
self.nick = None
self.roles = None
self.guest = True
self.alive = True
def _update_logger(self):
self.log = utils.get_logger(f"client({self.nick}:{self.cid})")
self.log.debug(f"Update logger")
def is_disconnected(self):
if not self.alive:
return True
res = self.writer.is_closing()
if res:
self.log.debug(f"Client Disconnected")
self.alive = False
return True
else:
self.log.debug(f"Client Alive")
self.alive = True
return False
async def kick(self, reason):
self.log.info(f"Client: \"IP: {self.addr!r}; ID: {self.cid}\" - kicked with reason: \"{reason}\"")
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
self.alive = False
await self.remove_me()
async def tcp_send(self, data, to_all=False, writer=None):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
if writer is None:
writer = self.writer
if to_all:
for client in self.Core.clients:
if not client:
continue
await client.tcp_send(data)
return
self.log.debug(f"tcp_send({data})")
if len(data) == 10:
data += b"."
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len(data) {len(data)}; send {header + data}')
try:
writer.write(header + data)
await writer.drain()
except ConnectionError:
self.log.debug('Disconnected')
self.alive = False
async def recv(self, kostil=False):
# if not self.is_disconnected():
# self.log.debug(f"Client with {self.nick}({self.cid}) disconnected")
# return b""
try:
header = await self.reader.read(4) # header: 4 bytes
int_header = 0
for i in range(len(header)):
int_header += header[i]
if int_header <= 0:
await asyncio.sleep(0.1)
if not self.alive:
self.log.debug(f"Disconnected")
self.writer.close()
return b''
if kostil:
return
self.log.debug(f"Header: {header}")
await self.kick("Invalid packet - header negative")
return b""
if int_header > 100 * MB:
await self.kick("Header size limit exceeded")
self.log.warn(f"Client {self.nick}({self.cid}) sent header of >100MB - "
f"assuming malicious intent and disconnecting the client.")
return b""
data = await self.reader.read(100 * MB)
self.log.debug(f"header: `{header}`; int_header: `{int_header}`; data: `{data}`;")
if len(data) != int_header:
self.log.debug(f"WARN Expected to read {int_header} bytes, instead got {len(data)}")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
self.log.debug(f"ABG: {data}")
return data
return data
except ConnectionError:
self.alive = False
await self.remove_me()
return b""
async def sync_resources(self):
while True:
data = await self.recv()
if not data:
await asyncio.sleep(.1)
continue
self.log.debug(f"Received: {data}")
if data.startswith(b"f"):
# TODO: SendFile
file = data[1:].decode("utf-8")
self.log.debug(f"Sending File: {file}")
await self.kick(f"TODO: SendFile({file})")
elif data.startswith(b"SR"):
self.log.debug("Sending Mod Info")
mod_list = '%s;%s;'
for mod in self.Core.mods_list:
if type(mod) == int:
continue
mod_list = (mod_list % (mod['path'], mod['size'])).replace(";", ";%s;")
mod_list = mod_list.replace("%s;", "")
if len(mod_list) == 0:
await self.tcp_send(b"-")
else:
await self.tcp_send(bytes(mod_list, "utf-8"))
data = await self.recv()
if data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
async def looper(self):
# self.is_disconnected()
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8"))
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
break
else:
await asyncio.sleep(.2)
continue
code = data.decode()[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
case "C":
# Chat
await self.tcp_send(data, to_all=True)
async def remove_me(self):
self.log.debug(f"Removing client {self.nick}({self.cid})")
await asyncio.sleep(0.3)
if not self.writer.is_closing():
self.writer.close()
if (self.cid > 0 or self.nick is not None) and self.Core.clients_by_id.get(self.cid):
_, down_w = self.down_rw
if down_w and not down_w.is_closing():
down_w.close()
self.Core.clients[self.cid] = None
self.Core.clients_by_id.pop(self.cid)
self.Core.clients_by_nick.pop(self.nick)
# noinspection PyProtectedMember
class Core:
def __init__(self):
@@ -220,11 +41,17 @@ class Core:
self.web_pool = webapp.data_pool
self.web_stop = None
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
self.lock_upload = False
def get_client(self, cid=None, nick=None):
if cid:
self.client_major_version = "2.0"
self.BeamMP_version = "3.1.1" # 20.07.2023
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:
return self.clients_by_nick.get(nick)
@@ -241,11 +68,12 @@ 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: id{client.cid}")
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
self.clients_by_id.update({client.cid: client})
self.clients[client.cid] = client
# noinspection PyProtectedMember
client._update_logger()
return
await self.insert_client(client)
@@ -270,17 +98,23 @@ class Core:
async def check_alive(self):
maxp = config.Game['players']
while self.run:
await asyncio.sleep(1)
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
for client in self.clients:
if not client:
continue
await client.tcp_send(bytes(ca, "utf-8"))
try:
while self.run:
await asyncio.sleep(1)
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
for client in self.clients:
if not client:
continue
if not client.ready:
client.is_disconnected()
continue
await client._send(ca)
except Exception as e:
self.log.error("Error in check_alive.")
self.log.exception(e)
@staticmethod
def start_web():
global uvserver
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app",
host=config.WebAPI["server_ip"],
port=config.WebAPI["server_port"],
@@ -299,6 +133,7 @@ class Core:
async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct:
if test:
# TODO: i18n
self.log.info(f"Server runnig in Direct connect mode.")
self.direct = True
return
@@ -313,10 +148,10 @@ class Core:
modstotal = len(self.mods_list) - 1
while self.run:
try:
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
"private": config.Auth['private'], "version": self.BeamMP_version,
"clientversion": self.client_major_version,
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
"version": self.BeamMP_version, "clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
self.log.debug(f"Auth: data {data}")
@@ -324,7 +159,6 @@ class Core:
# Sentry?
ok = False
body = {}
code = 0
for server_url in BEAM_backend:
url = "https://" + server_url + "/heartbeat"
try:
@@ -350,6 +184,7 @@ class Core:
status = body.get("status")
msg = body.get("msg")
if status == "2000":
# TODO: i18n
self.log.info(f"Authenticated! {msg}")
elif status == "200":
self.log.info(f"Resumed authenticated session. {msg}")
@@ -361,11 +196,11 @@ class Core:
else:
self.direct = True
if test:
self.log.error("Cannot auth...")
if not config.Auth['private']:
raise KeyboardInterrupt
if test:
# TODO: i18n
self.log.error("Cannot authenticate server.")
self.log.info(f"Server still runnig, but only in Direct connect mode.")
# if not config.Auth['private']:
# raise KeyboardInterrupt
if test:
return ok
@@ -375,21 +210,28 @@ class Core:
self.log.error(f"Error in heartbeat: {e}")
async def main(self):
self.run = True
self.tcp = self.tcp(self, self.server_ip, self.server_port)
self.udp = self.udp(self, self.server_ip, self.server_port)
console.add_command(
"list",
lambda x: f"Players list: {self.get_clients_list(True)}"
)
self.log.debug("Initializing PluginsLoader...")
if not os.path.exists("plugins"):
os.mkdir("plugins")
pl = PluginsLoader("plugins")
await pl.load()
try:
# WebApi Start
if config.WebAPI["enabled"]:
self.log.debug("Initializing WebAPI...")
web_thread = Thread(target=self.start_web)
web_thread = Thread(target=self.start_web, name="WebApiThread")
web_thread.start()
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
self.web_thread = web_thread
# noinspection PyProtectedMember
self.web_stop = webapp._stop
await asyncio.sleep(.3)
@@ -404,40 +246,49 @@ class Core:
self.mods_list.append({"path": path, "size": size})
self.mods_list[0] += size
self.log.debug(f"mods_list: {self.mods_list}")
lmods = len(self.mods_list) - 1
if lmods > 0:
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb")
len_mods = len(self.mods_list) - 1
if len_mods > 0:
# TODO: i18n
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
self.log.info(i18n.init_ok)
await self.heartbeat(True)
for i in range(int(config.Game["players"] * 1.3)):
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
self.clients.append(None)
tasks = []
# self.udp.start,
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
for task in nrtasks:
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
for task in f_tasks:
tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
await ev.call_async_event("_plugins_start")
self.run = True
self.log.info(i18n.start)
ev.call_event("on_started")
await t
# Wait the end.
ev.call_event("onServerStarted")
await ev.call_async_event("onServerStarted")
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.tcp.stop()
# self.udp.stop()
self.run = False
self.tcp.stop()
self.udp._stop()
await self.stop()
def start(self):
asyncio.run(self.main())
def stop(self):
async def stop(self):
ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped")
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

@@ -1,44 +1,19 @@
# Developed by KuiToi Dev
# File core.core.pyi
# Written by: SantaSpeen
# Version 0.2.0
# Version 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamWriter, StreamReader
from threading import Thread
from typing import Callable, List, Dict, Tuple
from typing import Callable, List, Dict
from core import utils
from .Client import Client
from .tcp_server import TCPServer
from .udp_server import UDPServer
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 = 0
self.key: str = None
self.nick: str = None
self.roles: str = None
self.guest = True
self.alive = True
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 looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def remove_me(self) -> None: ...
class Core:
def __init__(self):
self.log = utils.get_logger("core")
@@ -57,8 +32,10 @@ class Core:
self.udp = UDPServer
self.web_thread: Thread = None
self.web_stop: Callable = lambda: None
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
def get_client(self, cid=None, nick=None) -> Client | None: ...
async def insert_client(self, client: Client) -> None: ...
def create_client(self, *args, **kwargs) -> Client: ...
def get_clients_list(self, need_cid=False) -> str: ...
@@ -69,4 +46,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

@@ -1,19 +1,18 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.2.1
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import traceback
from asyncio import AbstractEventLoop
from threading import Thread
import aiohttp
from core import utils
# noinspection PyProtectedMember
class TCPServer:
def __init__(self, core, host, port):
self.log = utils.get_logger("TCPServer")
@@ -25,52 +24,62 @@ class TCPServer:
async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer)
# TODO: i18n
self.log.info(f"Identifying new ClientConnection...")
data = await client.recv()
self.log.debug(f"recv1 data: {data}")
data = await client._recv(True)
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, None
return False, client
else:
await client.tcp_send(b"S") # Accepted client version
await client._send(b"S") # Accepted client version
data = await client.recv()
self.log.debug(f"recv2 data: {data}")
data = await client._recv(True)
self.log.debug(f"Key: {data}")
if len(data) > 50:
# TODO: i18n
await client.kick("Invalid Key (too long)!")
return False, None
client.key = data.decode("utf-8")
return False, client
client._key = data.decode("utf-8")
ev.call_event("onPlayerSentKey", player=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, None
return False, client
client.nick = res["username"]
client.roles = res["roles"]
client.guest = res["guest"]
client._guest = res["guest"]
# noinspection PyProtectedMember
client._update_logger()
except Exception as e:
# TODO: i18n
self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
return False, None
return False, client
for _client in self.Core.clients:
if not _client:
continue
if _client.nick == client.nick and _client.guest == client.guest:
# TODO: i18n
await client.kick('Stale Client (replaced by new client)')
return False, None
return False, client
ev.call_event("on_auth", client)
ev.call_event("onPlayerAuthenticated", player=client)
if len(self.Core.clients_by_id) > config.Game["players"]:
# TODO: i18n
await client.kick("Server full!")
return False, None
return False, client
else:
# TODO: i18n
self.log.info("Identification success")
await self.Core.insert_client(client)
@@ -79,17 +88,13 @@ class TCPServer:
async def set_down_rw(self, reader, writer):
try:
cid = (await reader.read(1))[0]
ok = False
for _client in self.Core.clients:
if not _client:
continue
if _client.cid == cid:
_client.down_rw = (reader, writer)
ok = True
self.log.debug(f"Client: {_client.nick}:{cid} - HandleDownload!")
if not ok:
client = self.Core.get_client(cid=cid)
if client:
client._down_sock = (reader, writer)
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
else:
writer.close()
self.log.debug(f"Unknown client - HandleDownload")
self.log.debug(f"Unknown client id:{cid} - HandleDownload")
finally:
return
@@ -98,7 +103,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)
@@ -107,6 +112,7 @@ class TCPServer:
await writer.drain()
writer.close()
case _:
# TODO: i18n
self.log.error(f"Unknown code: {code}")
writer.close()
return False, None
@@ -123,10 +129,12 @@ 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:
self.log.error("Error while connecting..")
# TODO: i18n
self.log.error("Error while handling connection...")
self.log.exception(e)
traceback.print_exc()
break
@@ -136,15 +144,18 @@ class TCPServer:
self.run = True
try:
server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=int(config.Game["players"] * 1.3))
backlog=int(config.Game["players"] * 2.3))
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
while True:
async with server:
await server.serve_forever()
except OSError as e:
# 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

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.pyi
# Written by: SantaSpeen
# Version 0.2.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -9,7 +9,7 @@ from asyncio import StreamWriter, StreamReader
from typing import Tuple
from core import utils, Core
from core.core import Client
from core.Client import Client
class TCPServer:

View File

@@ -1,18 +1,20 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# Written by: SantaSpeen
# Version 0.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import traceback
from core import utils
class UDPServer:
# noinspection PyProtectedMember
class UDPServer(asyncio.DatagramTransport):
transport = None
def __init__(self, core, host, port):
def __init__(self, core, host=None, port=None):
super().__init__()
self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core
@@ -20,41 +22,77 @@ class UDPServer:
self.port = port
self.run = False
async def handle_client(self, reader, writer):
while True:
try:
data = await reader.read(1)
if not data:
break
code = data.decode()
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
# await self.handle_code(code, reader, writer)
# task = asyncio.create_task(self.handle_code(code, reader, writer))
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
if not writer.is_closing():
writer.close()
self.log.debug("Disconnected.")
break
except Exception as e:
self.log.error("Error while connecting..")
self.log.error(f"Error: {e}")
traceback.print_exc()
break
def connection_made(self, transport): ...
async def start(self):
self.log.debug("Starting UDP server.")
self.run = True
async def handle_datagram(self, data, addr):
try:
pass
cid = data[0] - 1
code = data[2:3].decode()
client = self.Core.get_client(cid=cid)
if client:
match code:
case "p": # Ping packet
ev.call_event("onSentPing")
self.transport.sendto(b"p", addr)
case "Z": # Position packet
if client._udp_sock != (self.transport, addr):
client._udp_sock = (self.transport, addr)
self.log.debug(f"Set UDP Sock for CID: {cid}")
ev.call_event("onChangePosition")
if client:
await client._send(data[2:], to_all=True, to_self=False, to_udp=True)
case _:
self.log.debug(f"[{cid}] Unknown code: {code}")
else:
self.log.debug(f"Client not found.")
except Exception as e:
self.log.error(f"Error handle_datagram: {e}")
def datagram_received(self, *args, **kwargs):
self.loop.create_task(self.handle_datagram(*args, **kwargs))
def connection_lost(self, exc):
if exc is not None and exc != KeyboardInterrupt:
self.log.debug(f'Connection raised: {exc}')
self.log.debug(f'Disconnected.')
def error_received(self, exc):
self.log.debug(f'error_received: {exc}')
self.log.exception(exc)
self.connection_lost(exc)
self.transport.close()
async def _start(self):
self.log.debug("Starting UDP server.")
try:
while self.Core.run:
await asyncio.sleep(0.2)
d = UDPServer
self.transport, p = await self.loop.create_datagram_endpoint(
lambda: d(self.Core),
local_addr=(self.host, self.port)
)
d.transport = self.transport
if not self.run:
self.log.debug(f"UDP server started on {self.transport.get_extra_info('sockname')}")
self.run = True
while not self.transport.is_closing():
await asyncio.sleep(0.2)
except OSError as e:
self.log.error("Cannot bind port or other error")
raise e
except BaseException as e:
self.log.exception(e)
except Exception as e:
self.log.error(f"Error: {e}")
raise e
self.log.exception(e)
finally:
self.run = False
self.Core.run = False
def stop(self):
def _stop(self):
self.log.debug("Stopping UDP server")
self.transport.close()

View File

@@ -1,24 +1,30 @@
# Developed by KuiToi Dev
# File core.udp_server.py
# Written by: SantaSpeen
# Version 0.0
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import DatagramTransport
from typing import Tuple, List
from core import utils
from core.core import Core
class UDPServer:
class UDPServer(asyncio.DatagramTransport):
transport: DatagramTransport = None
def __init__(self, core, host, port):
def __init__(self, core: Core, host=None, port=None, transport=None):
self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core
self.host = host
self.port = port
self.run = False
async def handle_client(self, srv_sock) -> None: ...
async def start(self) -> None: ...
async def stop(self) -> None: ...
# self.transport: DatagramTransport = None
def connection_made(self, transport: DatagramTransport): ...
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
async def _start(self) -> None: ...
async def _stop(self) -> None: ...

View File

@@ -2,6 +2,7 @@
# File core.utils.py
# Written by: SantaSpeen
# Version 1.1
# Core version: 0.4.1
# Licence: FPA
# (c) kuitoi.su 2023
import datetime
@@ -19,8 +20,8 @@ logging.basicConfig(level=log_level, format=log_format)
if not os.path.exists(log_dir):
os.mkdir(log_dir)
if os.path.exists(log_file):
mtime = os.path.getmtime(log_file)
gz_path = log_dir + datetime.datetime.fromtimestamp(mtime).strftime('%d.%m.%Y') + "-%s.tar.gz"
ftime = os.path.getmtime(log_file)
gz_path = log_dir + datetime.datetime.fromtimestamp(ftime).strftime('%d.%m.%Y') + "-%s.tar.gz"
index = 1
while True:
if not os.path.exists(gz_path % index):
@@ -32,11 +33,15 @@ if os.path.exists(log_file):
if os.path.exists(file):
tar.add(file, os.path.basename(file))
os.remove(file)
fh = logging.FileHandler(log_file, encoding='utf-8')
fh = logging.FileHandler(log_file, encoding="utf-8")
fh.setFormatter(logging.Formatter(log_format))
def get_logger(name):
try:
fh.encoding = config.enc
except NameError:
fh.encoding = "utf-8"
log = logging.getLogger(name=name)
log.addHandler(fh)
log.level = log_level

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

@@ -1,8 +1,17 @@
import secrets
class Config:
Auth: dict
Game: dict
Server: dict
WebAPI: dict
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "encoding": "utf8", "speed_limit": 0, "use_queue": False,
"debug": False}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"secret_key": secrets.token_hex(16)}
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ...

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.config_provider.config_provider.py
# Written by: SantaSpeen
# Version 1.0
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import os
@@ -11,13 +10,14 @@ import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, web=None):
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "language": "en",
"server_ip": "0.0.0.0", "server_port": 30814, "debug": False}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
"debug": False}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"secret_key": secrets.token_hex(16)}

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File core.config_provider.py
# File modules.ConsoleSystem.console_system.py
# Written by: SantaSpeen
# Version 1.1
# Version 1.2
# Licence: FPA
# (c) kuitoi.su 2023
import builtins
@@ -87,7 +87,7 @@ class Console:
print()
raw = True
message = str()
message = "\n"
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7

View File

@@ -1,46 +1,108 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.EventsSystem.events_system.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import builtins
import inspect
from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem:
def __init__(self):
self.__events = {
"on_started": [self.on_started],
"on_stop": [self.on_stop],
"on_auth": [self.on_auth]
}
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"onServerStarted": [],
"onPlayerSentKey": [], # Only sync
"onPlayerAuthenticated": [], # Only sync
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onSentPing": [], # Only sync
"onChangePosition": [], # Only sync
"onServerStopped": [],
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onServerStopped": []
}
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 event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
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:
self.__events[event_name].append(event_func)
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
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.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
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, maybe ev.call_event()?. Just skipping it...")
return funcs_data
def call_event(self, event_name, *args, **kwargs):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
def call_event(self, event_name, *data):
self.log.debug(f"Using event '{event_name}'")
if event_name in self.__events.keys():
for event in self.__events[event_name]:
event(*data)
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:
self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
def on_started(self):
pass
def on_stop(self):
pass
def on_auth(self, client):
pass
return funcs_data

View File

@@ -1,6 +1,11 @@
from typing import Any
class EventsSystem:
@staticmethod
def register_event(self, event_name, event_func): ...
def register_event(event_name, event_func): ...
@staticmethod
def call_event(self, event_name, *data): ...
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,52 +1,196 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PluginsLoader.plugins_loader.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import inspect
import os
import types
from contextlib import contextmanager
from threading import Thread
from core import get_logger
# TODO: call_client_event, get_player, get_players, GetPlayerCount
class KuiToi:
_plugins_dir = ""
def __init__(self, name=None):
if name is None:
raise Exception("BeamMP: Name is required")
self.log = get_logger(f"PluginsLoader | {name}")
self.name = name
raise AttributeError("KuiToi: Name is required")
self.log = get_logger(f"Plugin | {name}")
self.__name = name
self.__dir = os.path.join(self._plugins_dir, self.__name)
if not os.path.exists(self.__dir):
os.mkdir(self.__dir)
def set_name(self, name):
self.name = name
@property
def name(self):
return self.__name
@staticmethod
def register_event(event_name, event_func):
@name.setter
def name(self, value):
# You chell not pass
pass
@property
def dir(self):
return self.__dir
@dir.setter
def dir(self, value):
# You chell not pass
pass
@contextmanager
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
path = os.path.join(self.__dir, file)
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
# if not os.path.exists(path):
# with open(path, 'x'): ...
f = None
try:
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
yield f
except Exception as e:
raise e
finally:
if f is not None:
f.close()
def register_event(self, event_name, event_func):
self.log.debug(f"Registering event {event_name}")
ev.register_event(event_name, event_func)
@staticmethod
def call_event(event_name, *data):
ev.call_event(event_name, *data)
def call_event(self, event_name, *data, **kwargs):
self.log.debug(f"Called event {event_name}")
ev.call_event(event_name, *data, **kwargs)
class PluginsLoader:
def __init__(self, plugins_dir):
self.__plugins = {}
self.__plugins_dir = plugins_dir
self.loop = asyncio.get_event_loop()
self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
self.loaded_str = "Plugins: "
ev.register_event("_plugins_start", self.start)
ev.register_event("_plugins_unload", self.unload)
console.add_command("plugins", lambda x: self.loaded_str[:-2])
console.add_command("pl", lambda x: self.loaded_str[:-2])
def load_plugins(self):
async def load(self):
self.log.debug("Loading plugins...")
files = os.listdir(self.__plugins_dir)
files = os.listdir(self.plugins_dir)
for file in files:
if file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file}")
plugin = types.ModuleType('plugin')
self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print
file = os.path.join(self.__plugins_dir, file)
with open(f'{file}', 'r') as f:
code = f.read().replace("import KuiToi\n", "")
file_path = os.path.join(self.plugins_dir, file)
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)
plugin.load()
self.__plugins.update({file[:-3]: plugin})
self.log.debug(f"Plugin loaded: {file}")
ok = True
try:
isfunc = inspect.isfunction
if not isfunc(plugin.load):
self.log.error('Function "def load():" not found.')
ok = False
if not isfunc(plugin.start):
self.log.error('Function "def start():" not found.')
ok = False
if not isfunc(plugin.unload):
self.log.error('Function "def unload():" not found.')
ok = False
if type(plugin.kt) != KuiToi:
self.log.error(f'Attribute "kt" isn\'t KuiToi class. Plugin file: "{file_path}"')
ok = False
except AttributeError:
ok = False
if not ok:
self.log.error(f'Plugin file: "{file_path}" is not a valid KuiToi plugin.')
return
pl_name = plugin.kt.name
if self.plugins.get(pl_name) is not None:
raise NameError(f'Having plugins with identical names is not allowed; '
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
plugin.open = plugin.kt.open
iscorfunc = inspect.iscoroutinefunction
self.plugins.update(
{
pl_name: {
"plugin": plugin,
"load": {
"func": plugin.load,
"async": iscorfunc(plugin.load)
},
"start": {
"func": plugin.start,
"async": iscorfunc(plugin.start)
},
"unload": {
"func": plugin.unload,
"async": iscorfunc(plugin.unload)
}
}
}
)
if self.plugins[pl_name]["load"]['async']:
plugin.log.debug(f"I'm async")
await plugin.load()
else:
plugin.log.debug(f"I'm sync")
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
th.start()
th.join()
self.loaded_str += f"{pl_name}:ok, "
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
except Exception as e:
self.log.error(f"Error loading plugin: {file}; Error: {e}")
# TODO: i18n
self.loaded_str += f"{file}:no, "
self.log.error(f"Error while loading plugin: {file}; Error: {e}")
self.log.exception(e)
async def start(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['start']['async']:
self.log.debug(f"Start async plugin: {pl_name}")
t = self.loop.create_task(pl_data['start']['func']())
self.plugins_tasks.append(t)
else:
self.log.debug(f"Start sync plugin: {pl_name}")
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
th.start()
self.plugins_tasks.append(th)
except Exception as e:
self.log.exception(e)
async def unload(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['unload']['async']:
self.log.debug(f"Unload async plugin: {pl_name}")
await pl_data['unload']['func']()
else:
self.log.debug(f"Unload sync plugin: {pl_name}")
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
th.start()
th.join()
except Exception as e:
self.log.exception(e)

View File

@@ -122,6 +122,5 @@ def hack_fastapi():
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
print(LOGGING_CONFIG)

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

@@ -3,7 +3,7 @@
# Developed by KuiToi Dev
# File modules.i18n.i18n.py
# Written by: SantaSpeen
# Version 1.0
# Version 1.3
# Licence: FPA
# (c) kuitoi.su 2023
import builtins
@@ -68,7 +68,9 @@ class i18n:
class MultiLanguage:
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
self.__data = {}
@@ -89,53 +91,53 @@ class MultiLanguage:
else:
# noinspection PyDictDuplicateKeys
self.__data = {
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"start": "Server started!",
"stop": "Goodbye!",
"": "Basic phases",
"hello": "Greetings from KuiToi Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization complete.",
"start": "Server started!",
"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_use_link": "Use this link: {}",
"": "Server auth",
"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",
"GUI_yes": "Yes",
"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 phases",
"GUI_yes": "Yes",
"GUI_no": "No",
"GUI_ok": "Ok",
"GUI_cancel": "Cancel",
"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 phases",
"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!",
"": "Command: man",
"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",
"help_command": "Command",
"help_message": "Help message",
"help_message_not_found": "No help message found",
"": "Command: help",
"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": "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.",
"": "Command: stop",
"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."
}
"": "Command: exit",
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}
self.__i18n = i18n(self.__data)
def open_file(self):