61 Commits

Author SHA1 Message Date
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
dd2c461581 Update TODOs 2023-07-13 17:55:13 +03:00
a8c153691c 0.2.0 -> 0.2.1 2023-07-13 17:50:15 +03:00
52893513d0 Prepare for sync_resources;
Fix async bugs;
Recreate ID system;
Add Ss (Player counter) code;
2023-07-13 17:49:23 +03:00
1f595db700 Handle return from callback 2023-07-13 16:49:19 +03:00
565750e784 Refactor console logger 2023-07-13 16:44:05 +03:00
13321fb9b5 Minor update 2023-07-13 13:29:37 +03:00
cdf226ac5c Update TODOs 2023-07-13 11:41:00 +03:00
dcafef918a Minor naming update 2023-07-13 09:53:57 +03:00
6e46af4c13 Prepare for Upload mods 2023-07-13 02:35:38 +03:00
d21798aaf1 Minor fix 2023-07-13 02:34:56 +03:00
22105b2030 Minor fix 2023-07-13 02:34:25 +03:00
19c121f208 Refactor Client ID 2023-07-13 02:33:45 +03:00
85c379bd9e Minor fixes 2023-07-13 01:17:01 +03:00
a15eb316bb Update TODOs 2023-07-13 01:16:10 +03:00
cecd6f13d6 Handle FastApi Log in file 2023-07-13 00:44:51 +03:00
df171aaa70 Minor fix 2023-07-13 00:31:02 +03:00
5a1cb8a133 Handle web logs 2023-07-13 00:23:57 +03:00
d44cff1116 Update version 2023-07-12 23:58:35 +03:00
bc6cf60099 Update TODOs 2023-07-12 23:58:23 +03:00
fc886ef415 Create log history 2023-07-12 23:58:14 +03:00
bd7b988b01 logs 2023-07-12 23:39:52 +03:00
37 changed files with 1575 additions and 669 deletions

1
.gitignore vendored
View File

@@ -137,3 +137,4 @@ dmypy.json
/src/plugins /src/plugins
/test/ /test/
*test.py *test.py
logs/

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: 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. 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

@@ -11,17 +11,21 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Private access (Without key, Direct connect) - [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher) - [x] Public access (With key, listing in Launcher)
- [X] Player authentication - [X] Player authentication
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] TCP Server part: - [ ] TCP Server part:
- [x] Handle code - [x] Handle code
- [x] Understanding BeamMP header - [x] Understanding BeamMP header
- [ ] Upload mods - [x] Upload mods
- [x] Connecting to the world - [x] Connecting to the world
- [x] Chat - [x] Chat
- [ ] Player counter _(Code: Ss)_ - [x] Players online counter
- [ ] Car state synchronizations _(Codes: We, Vi)_ - [ ] Car state synchronizations _(Codes: We, Vi)_
- [ ] "ABG:" (compressed data) - [ ] "ABG:" (compressed data)
- [x] Decompress data - [x] Decompress data
- [ ] Vehicle data - [ ] Vehicle data _(Code: Os)_
- [ ] UDP Server part: - [ ] UDP Server part:
- [ ] Players synchronizations _(Code: Zp)_ - [ ] Players synchronizations _(Code: Zp)_
- [ ] Ping _(Code: p)_ - [ ] Ping _(Code: p)_
@@ -29,16 +33,19 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Logger - [x] Logger
- [x] Just logging - [x] Just logging
- [x] Log in file - [x] Log in file
- [ ] Lig history (.1.log, .2.log, ...) - [x] Log history (.1.log, .2.log, ...)
- [x] Console: - [x] Console:
- [x] Tabulation - [x] Tabulation
- [ ] _(Deferred)_ Static text (bug) - [x] _~~(By design)~~_ Static text (bug)
- [x] Events System - [x] Events System
- [x] Call events - [x] Call events
- [x] Create custom events - [x] Create custom events
- [ ] Return from events - [x] Return from events
- [x] Async support
- [x] Plugins support - [x] Plugins support
- [ ] KuiToi class
- [x] Load Python plugins - [x] Load Python plugins
- [x] Async support
- [ ] Load Lua plugins (Original BeamMP compatibility) - [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support) - [x] MultiLanguage (i18n support)
- [x] Core - [x] Core
@@ -46,9 +53,10 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] WebAPI - [x] WebAPI
- [x] HTTP API Server (fastapi) - [x] HTTP API Server (fastapi)
- [x] Stop and Start with core - [x] Stop and Start with core
- [x] Custom logger - [x] Configure FastAPI logger
- [ ] Sync with event system - [ ] Sync with event system
- [ ] [Documentation](docs/en/readme.md) - [ ] Add methods...
- [ ] [Documentation](./docs/)
## Installation ## Installation
@@ -73,6 +81,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: 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. - 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: 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. 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", "": "Basic phases",
"hello": "Hello from KuiToi-Server!", "hello": "Greetings from KuiToi Server!",
"config_path": "Use {} for config.", "config_path": "Use {} to configure.",
"init_ok": "Initializing ready.", "init_ok": "Initialization complete.",
"start": "Server started!", "start": "Server started!",
"stop": "Goodbye!", "stop": "Server stopped!",
"": "Server auth", "": "Server auth",
"auth_need_key": "BEAM key needed for starting the server!", "auth_need_key": "A BeamMP key is required to start the server!",
"auth_empty_key": "Key is empty!", "auth_empty_key": "The BeamMP key is empty!",
"auth_cannot_open_browser": "Cannot open browser: {}", "auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}", "auth_use_link": "Use this link: {}",
"": "GUI phases", "": "GUI phases",
@@ -17,32 +17,32 @@
"GUI_no": "No", "GUI_no": "No",
"GUI_ok": "Ok", "GUI_ok": "Ok",
"GUI_cancel": "Cancel", "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_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 type your key:", "GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}", "GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases", "": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)", "web_start": "WebAPI started at {} (Press CTRL+C to quit)",
"": "Command: man", "": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND", "man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.", "help_message_man": "Displays help page for COMMAND.",
"man_for": "Manual for command", "man_for": "Help page for",
"man_message_not_found": "man: Manual message not found.", "man_message_not_found": "man: Help page not found.",
"man_command_not_found": "man: command \"{}\" not found!", "man_command_not_found": "man: Command \"{}\" not found!",
"": "Command: help", "": "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.", "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": "Display names and brief descriptions of available commands", "help_message_help": "Displays the names and short descriptions of commands.",
"help_command": "Command", "help_command": "Command",
"help_message": "Help message", "help_message": "Description",
"help_message_not_found": "No help message found", "help_message_not_found": "No description available.",
"": "Command: stop", "": "Command: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop", "man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Server shutdown.", "help_message_stop": "Stops the server.",
"": "Command: exit", "": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop", "man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Server shutdown." "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" try:
log = beam.log # Use logger from server 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(): def my_event_handler(event_data):
# When plugin initialization Server uses plugin.load() to load plugin. log.info(f"{event_data}")
# def load(): is really needed
log.info(beam.name)
# Events handlers def load():
# Инициализация плагина
def on_started(): with open(cfg_file, 'w') as f:
# Simple event handler json.dump(config, f)
log.info("Server starting...") cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
# Simple event register def start():
beam.register_event("on_started", on_started) # Запуск процессов плагина
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
def any_func(data=None): def unload():
# Custom event handler # Код завершающий все процессы
log.info(f"Data from any_func: {data}") log.info("Плагин выгружен успешно.")
# 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")

View File

@@ -1,32 +1,92 @@
# Plugins System # Plugin System
## Install ## Installing the Library with "Stubs"
###### (Lib can't ready to use) ###### (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` `$ pip install KuiToi`
* From source:\ * From source code:\
`git clone https://github.com/KuiToi/KuiToi-PyLib` `git clone https://github.com/KuiToi/KuiToi-PyLib`
## Example ## Example
```python ```python
import KuiToi try:
import KuiToi
except ImportError:
pass
beam = KuiToi("TestPlugin") kt = KuiToi("Example")
logger = beam.log log = kt.log
def load(): # Plugins load from here def my_event_handler(event_data):
print(beam.name) log.info(f"{event_data}")
def on_started(): def load():
logger.info("Server starting...") # 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'] * It is recommended to use `open()` after `load()`. Otherwise, use `kt.load()` - creates a file in the `plugin/<plugin_name>/<filename>` folder.
* Create new event : `beam.register_event("my_event", my_event_function)` * Creating your own event: `kt.register_event("my_event", my_event_function)`
* Call event: `beam.call_event("my_event")` * Calling an event: `kt.call_event("my_event")`
* Call event with some data: `beam.call_event("my_event", data, data2)` * Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
* Calls _**can't support**_ like this: `beam.call_event("my_event", data=data)` * 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 # 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) 1. Setup and Start server - [here](setup)
2. Plugins and Events system - [here](plugins) 2. Plugins and Events system - [here](plugins)
3. MultiLanguage - [here](./multilanguage) 3. MultiLanguage - [here](./multilanguage)
4. RESP API - [here](./web) 4. KuiToi WebAPI - [here](./web)
5. Something new 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! ###### _(Here are the commands for Linux)_
* After cloning use this:
* **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 ```bash
$ python3 --version # Python 3.10.6 python3 --version # Python 3.10.6
$ python3 main.py --help # Show help message ```
$ python3 main.py # Start server * 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 ```yaml
!!python/object:modules.ConfigProvider.config_provider.Config !!python/object:modules.ConfigProvider.config_provider.Config
Auth: Auth:
@@ -23,11 +37,39 @@ Game:
max_cars: 1 max_cars: 1
players: 8 players: 8
Server: Server:
debug: true debug: false
description: This server uses KuiToi! description: Welcome to KuiToi Server!
language: en
name: KuiToi-Server name: KuiToi-Server
server_ip: 0.0.0.0 server_ip: 0.0.0.0
server_port: 30814 server_port: 30813
``` WebAPI:
* Server can't start without BEAM Auth.key 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" try:
log = beam.log # Use logger from server 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(): def my_event_handler(event_data):
# When plugin initialization Server uses plugin.load() to load plugin. log.info(f"{event_data}")
# def load(): is really needed
log.info(beam.name)
# Events handlers def load():
# Инициализация плагина
def on_started(): with open(cfg_file, 'w') as f:
# Simple event handler json.dump(config, f)
log.info("Server starting...") cgf = config
log.info(cgf)
ev.register_event("my_event", my_event_handler)
log.info("Плагин загружен успешно.")
# Simple event register def start():
beam.register_event("on_started", on_started) # Запуск процессов плагина
ev.call_event("my_event")
ev.call_event("my_event", "Some data", data="some data too")
log.info("Плагин запустился успешно.")
def any_func(data=None): def unload():
# Custom event handler # Код завершающий все процессы
log.info(f"Data from any_func: {data}") log.info("Плагин выгружен успешно.")
# 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")

View File

@@ -12,24 +12,81 @@
## Пример ## Пример
```python ```python
import KuiToi try:
import KuiToi
except ImportError:
pass
beam = KuiToi("TestPlugin") kt = KuiToi("Example")
logger = beam.log log = kt.log
def load(): # Plugins load from here def my_event_handler(event_data):
print(beam.name) log.info(f"{event_data}")
def on_started(): def load():
logger.info("Server starting...") # Инициализация плагина
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'] ## Async функции
* Создание своего ивента : `beam.register_event("my_event", my_event_function)`
* Вызов ивента: `beam.call_event("my_event")` Поддержка async есть
* Вызов ивента с данными: `beam.call_event("my_event", data, data2)`
* Вызовы с указанием переменой _**не поддерживаются**_: `beam.call_event("my_event", data=data)` ```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) 1. Настройка и Запуск сервера - [тута](./setup)
2. Плагины и Ивент система - [тута](./plugins) 2. Плагины и Ивент система - [тута](./plugins)
3. Мультиязычность - [тута](./multilanguage) 3. Мультиязычность - [тута](./multilanguage)
4. RESP API - [тута](./web) 4. KuiToi WebAPI - [тута](./web)
5. Тут будет что-то ново.... 5. Тут будет что-то ново....

View File

@@ -38,12 +38,39 @@ Game:
players: 8 players: 8
Server: Server:
debug: false debug: false
description: This server uses KuiToi! description: Welcome to KuiToi Server!
language: en
name: KuiToi-Server name: KuiToi-Server
server_ip: 0.0.0.0 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 ключ, без него не запустится. * Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
* Введя BeamMP ключ сервер появится в списке лаунчера. * Введя BeamMP ключ сервер появится в списке лаунчера.
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys) * Взять ключ можно тут: [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` - Ключ, который указан в конфигурации сервера
В разработке

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

@@ -0,0 +1,360 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.2.3
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import math
import zlib
from core import utils
class Client:
def __init__(self, reader, writer, core):
self.__reader = reader
self.__writer = writer
self._down_rw = (None, None)
self.__Core = core
self.__alive = True
self._loop = asyncio.get_event_loop()
self._log = utils.get_logger("client(None:0)")
self._addr = writer.get_extra_info("sockname")
self._cid = -1
self._key = None
self.nick = None
self.roles = None
self._guest = True
self._ready = False
self._cars = []
@property
def _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
res = self.__writer.is_closing()
if res:
self.log.debug(f"Disconnected.")
self.__alive = False
return True
else:
self.log.debug(f"Alive.")
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(b"K" + bytes(reason, "utf-8"))
self.__alive = False
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, "utf-8")
if writer is None:
writer = self.__writer
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']:
if code in ['O', 'T'] or len(data) > 1000:
# TODO: Compress data
await client._send(data)
else:
await client._send(data)
else:
# TODO: UDP send
self.log.debug(f"UDP Part not ready: {code}")
return
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len: {len(data)}; send: {header + data!r}')
try:
writer.write(header + data)
await writer.drain()
except ConnectionError:
self.log.debug('tcp_send: Disconnected')
self.__alive = False
async def _recv(self):
try:
header = await self.__reader.read(4)
int_header = 0
for i in range(len(header)):
int_header += header[i]
if int_header <= 0:
await asyncio.sleep(0.1)
self.is_disconnected()
if self.__alive:
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
return b""
async def _split_load(self, start, end, d_sock, filename):
# TODO: Speed limiter
real_size = end - start
writer = self._down_rw[1] if d_sock else self.__writer
who = 'dwn' if d_sock else 'srv'
if config.Server["debug"]:
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
with open(filename, 'rb') as f:
f.seek(start)
data = f.read(end)
try:
writer.write(data)
await writer.drain()
self.log.debug(f"[{who}] File sent.")
except ConnectionError:
self.__alive = False
self.log.debug(f"[{who}] Disconnected.")
return real_size
async def _sync_resources(self):
while self.__alive:
data = await self._recv()
self.log.debug(f"data: {data!r}")
if data.startswith(b"f"):
file = data[1:].decode("utf-8")
# TODO: i18n
self.log.info(f"Requested mode: {file!r}")
size = -1
for mod in self.__Core.mods_list:
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_rw[0]:
await asyncio.sleep(0.1)
t += 1
if t > 50:
await self.kick("Missing download socket")
return
half_size = math.floor(size / 2)
uploads = [
self._split_load(0, half_size, False, file),
self._split_load(half_size, size, True, file)
]
sl0, sl1 = await asyncio.gather(*uploads)
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.")
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(bytes(mod_list, "utf-8"))
elif data == b"Done":
await self._send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
return
async def _looper(self):
await self._send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
await self._sync_resources()
while self.__alive:
data = await self._recv()
if not data:
self.__alive = False
break
# V to Y
if 89 >= data[0] >= 86:
await self._send(data, to_all=True, to_self=False)
code = chr(data[0])
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
ev.call_event("player_join", player=self)
await ev.call_async_event("player_join", 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
# TODO: Sync cars
# for client in self.__Core.clients:
# for car in client.cars:
# await self._tcp_send(car)
case "C":
# Chat
msg = data.decode()[4 + len(self.nick):]
if not msg:
self.log.debug("Tried to send an empty event, ignoring")
continue
self.log.info(f"Received message: {msg}")
# TODO: Handle chat event
to_ev = {"message": msg, "player": self}
ev_data_list = ev.call_event("chat_receive", **to_ev)
d2 = await ev.call_async_event("chat_receive", **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:
if need_send:
need_send = False
to_all = True
if to_all:
if need_send:
need_send = False
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:
writer = to_client._writer
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}")
if need_send:
await self._send(data, to_all=True)
case "O":
# TODO: ParseVehicle
pass
case "E":
# TODO: HandleEvent
pass
case "N":
# TODO: N
pass
case _:
pass
async def _remove_me(self):
await asyncio.sleep(0.3)
self.__alive = False
if (self.cid > 0 or self.nick is not None) and \
self.__Core.clients_by_nick.get(self.nick):
# if self.ready:
# await self.tcp_send(b"", to_all=True) # I'm disconnected.
self.log.debug(f"Removing client {self.nick}:{self.cid}")
# TODO: i18n
self.log.info("Disconnected")
self.__Core.clients[self.cid] = None
self.__Core.clients_by_id.pop(self.cid)
self.__Core.clients_by_nick.pop(self.nick)
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_rw
if down_w and not down_w.is_closing():
down_w.close()
except Exception as e:
self.log.debug(f"Error while closing download writer: {e}")

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

@@ -0,0 +1,54 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.2.3
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter
from logging import Logger
from typing import Tuple
from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.__reader = reader
self.__writer = writer
self._down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self._log = utils.get_logger("client(id: )")
self._addr = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self.__Core = core
self._cid: int = -1
self._key: str = None
self.nick: str = None
self.roles: str = None
self._guest = True
self.__alive = True
self._ready = False
@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: ...
def is_disconnected(self) -> bool: ...
async def kick(self, reason: 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) -> bytes: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str) -> 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 # File core.__init__.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.3 # Version 1.3
# Core version: 0.2.0 # Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai) # Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
@@ -10,8 +10,8 @@
__title__ = 'KuiToi-Server' __title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.' __description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server' __url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.2.0' __version__ = '0.2.3'
__build__ = 776 __build__ = 1208 # Я это считаю лог файлами
__author__ = 'SantaSpeen' __author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su' __author_email__ = 'admin@kuitoi.su'
__license__ = "FPA" __license__ = "FPA"
@@ -19,7 +19,6 @@ __copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
import asyncio import asyncio
import builtins import builtins
import os
import webbrowser import webbrowser
import prompt_toolkit.shortcuts as shortcuts import prompt_toolkit.shortcuts as shortcuts
@@ -27,7 +26,7 @@ import prompt_toolkit.shortcuts as shortcuts
from .utils import get_logger from .utils import get_logger
from core.core import Core from core.core import Core
from main import parser from main import parser
from modules import ConfigProvider, EventsSystem, PluginsLoader from modules import ConfigProvider, EventsSystem
from modules import Console from modules import Console
from modules import MultiLanguage from modules import MultiLanguage
@@ -107,16 +106,8 @@ console.builtins_hook()
console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop) 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) 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.B = 1
builtins.KB = B * 1024 builtins.KB = B * 1024
builtins.MB = KB * 1024 builtins.MB = KB * 1024
builtins.GB = MB * 1024 builtins.GB = MB * 1024
builtins.TB = GB * 1024 builtins.TB = GB * 1024
log.info(i18n.init_ok)

View File

@@ -1,145 +1,25 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.core.py # File core.core.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.2.0 # Version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
import os import os
import zlib import random
from threading import Thread from threading import Thread
import aiohttp import aiohttp
import uvicorn import uvicorn
from core import utils from core import utils
from core.Client import Client
from core.tcp_server import TCPServer from core.tcp_server import TCPServer
from core.udp_server import UDPServer from core.udp_server import UDPServer
from modules import PluginsLoader
from modules.WebAPISystem import app as webapp from modules.WebAPISystem import app as webapp
class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
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.debug(f"Update logger")
self.log = utils.get_logger(f"client({self.nick}:{self.cid})")
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.writer.close()
# await self.writer.wait_closed()
self.alive = False
async def tcp_send(self, data):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
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}')
self.writer.write(header + data)
await self.writer.drain()
async def recv(self):
# if not self.is_disconnected():
# self.log.debug(f"Client with {self.nick}({self.cid}) disconnected")
# return b""
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 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(101 * 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
async def sync_resources(self):
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8"))
data = await self.recv()
if data.startswith(b"SR"):
await self.tcp_send(b"-") # Cannot handle mods for now.
data = await self.recv()
if data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
await self.last_handle()
async def last_handle(self):
# self.is_disconnected()
self.log.debug(f"Alive: {self.alive}")
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
break
elif self.is_disconnected():
break
else:
continue
code = data.decode()[0]
self.log.debug(f"Code: {code}, data: {data}")
match code:
case "H":
# Client connected
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"))
case "C":
# Chat
await self.tcp_send(data)
class Core: class Core:
def __init__(self): def __init__(self):
@@ -147,8 +27,9 @@ class Core:
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.run = False self.run = False
self.direct = False self.direct = False
self.clients = {} self.clients = []
self.clients_counter = 0 self.clients_by_id = {}
self.clients_by_nick = {}
self.mods_dir = "./mods" self.mods_dir = "./mods"
self.mods_list = [0, ] self.mods_list = [0, ]
self.server_ip = config.Server["server_ip"] self.server_ip = config.Server["server_ip"]
@@ -162,35 +43,71 @@ class Core:
self.client_major_version = "2.0" self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0" self.BeamMP_version = "3.2.0"
def get_client(self, sock=None, cid=None): ev.register_event("get_player", self.get_client)
if cid:
return self.clients.get(cid)
if sock:
return self.clients.get(sock.getsockname())
def insert_client(self, client): def get_client(self, cid=None, nick=None, from_ev=None):
self.log.debug(f"Inserting client: {client.cid}") if from_ev is not None:
self.clients.update({client.cid: client, client.nick: client}) 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)
async def insert_client(self, client):
await asyncio.sleep(random.randint(3, 9) * 0.01)
cid = 0
for _client in self.clients:
if not _client:
break
if _client.cid == cid:
cid += 1
else:
break
await asyncio.sleep(random.randint(3, 9) * 0.01)
if not self.clients[cid]:
client._cid = cid
self.clients_by_nick.update({client.nick: client})
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
self.clients_by_id.update({client.cid: client})
self.clients[client.cid] = client
# noinspection PyProtectedMember
client._update_logger()
return
await self.insert_client(client)
def create_client(self, *args, **kwargs): def create_client(self, *args, **kwargs):
client = Client(*args, **kwargs) self.log.debug(f"Create client")
self.clients_counter += 1 client = Client(core=self, *args, **kwargs)
client.id = self.clients_counter
client._update_logger()
self.log.debug(f"Create client: {client.cid}; clients_counter: {self.clients_counter}")
return client return client
def get_clients_list(self, need_cid=False):
out = ""
for client in self.clients:
if not client:
continue
out += f"{client._nick}"
if need_cid:
out += f":{client.cid}"
out += ","
if out:
out = out[:-1]
return out
async def check_alive(self): async def check_alive(self):
await asyncio.sleep(5) maxp = config.Game['players']
self.log.debug(f"Checking if clients is alive") while self.run:
for cl in self.clients.values(): await asyncio.sleep(1)
d = await cl.is_disconnected() ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
if d: for client in self.clients:
self.log.debug(f"Client ID: {cl.id} died...") if not client:
continue
if not client.ready:
client.is_disconnected()
continue
await client._send(bytes(ca, "utf-8"))
@staticmethod @staticmethod
def start_web(): def start_web():
global uvserver
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app", uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app",
host=config.WebAPI["server_ip"], host=config.WebAPI["server_ip"],
port=config.WebAPI["server_port"], port=config.WebAPI["server_port"],
@@ -199,16 +116,17 @@ class Core:
webapp.uvserver = uvserver webapp.uvserver = uvserver
uvserver.run() uvserver.run()
@staticmethod async def stop_me(self):
async def stop_me():
while webapp.data_run[0]: while webapp.data_run[0]:
await asyncio.sleep(1) await asyncio.sleep(1)
self.run = False
raise KeyboardInterrupt raise KeyboardInterrupt
# noinspection SpellCheckingInspection,PyPep8Naming # noinspection SpellCheckingInspection,PyPep8Naming
async def authenticate(self, test=False): async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct: if config.Auth["private"] or self.direct:
if test: if test:
# TODO: i18n
self.log.info(f"Server runnig in Direct connect mode.") self.log.info(f"Server runnig in Direct connect mode.")
self.direct = True self.direct = True
return return
@@ -222,77 +140,94 @@ class Core:
modstotalsize = self.mods_list[0] modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1 modstotal = len(self.mods_list) - 1
while self.run: while self.run:
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"], try:
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json", data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
"private": config.Auth['private'], "version": self.BeamMP_version, "port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
"clientversion": self.client_major_version, "private": config.Auth['private'], "version": self.BeamMP_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize, "clientversion": self.client_major_version,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False} "name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
self.log.debug(f"Auth: data {data}") "modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
self.log.debug(f"Auth: data {data}")
# Sentry? # Sentry?
ok = False ok = False
body = {} body = {}
code = 0 for server_url in BEAM_backend:
for server_url in BEAM_backend: url = "https://" + server_url + "/heartbeat"
url = "https://" + server_url + "/heartbeat" try:
try: async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession() as session: async with session.post(url, data=data, headers={"api-v": "2"}) as response:
async with session.post(url, data=data, headers={"api-v": "2"}) as response: code = response.status
code = response.status body = await response.json()
body = await response.json() self.log.debug(f"Auth: code {code}, body {body}")
self.log.debug(f"Auth: code {code}, body {body}") ok = True
ok = True break
break except Exception as e:
except Exception as e: self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`") continue
continue
if ok: if ok:
if not (body.get("status") is not None and if not (body.get("status") is not None and
body.get("code") is not None and body.get("code") is not None and
body.get("msg") is not None): body.get("msg") is not None):
self.log.error("Missing/invalid json members in backend response") self.log.error("Missing/invalid json members in backend response")
raise KeyboardInterrupt raise KeyboardInterrupt
if test: if test:
status = body.get("status") status = body.get("status")
msg = body.get("msg") msg = body.get("msg")
if status == "2000": if status == "2000":
self.log.info(f"Authenticated! {msg}") # TODO: i18n
elif status == "200": self.log.info(f"Authenticated! {msg}")
self.log.info(f"Resumed authenticated session. {msg}") elif status == "200":
else: self.log.info(f"Resumed authenticated session. {msg}")
self.log.error(f"Backend REFUSED the auth key. Reason: " else:
f"{msg or 'Backend did not provide a reason'}") self.log.error(f"Backend REFUSED the auth key. Reason: "
f"{msg or 'Backend did not provide a reason'}")
self.log.info(f"Server still runnig, but only in Direct connect mode.")
self.direct = True
else:
self.direct = True
if test:
self.log.error("Cannot auth...")
if not config.Auth['private']:
raise KeyboardInterrupt
if test:
# TODO: i18n
self.log.info(f"Server still runnig, but only in Direct connect mode.") self.log.info(f"Server still runnig, but only in Direct connect mode.")
self.direct = True
else:
self.direct = True
if test:
self.log.error("Cannot auth...")
if not config.Auth['private']:
raise KeyboardInterrupt
if test:
self.log.info(f"Server still runnig, but only in Direct connect mode.")
if test: if test:
return ok return ok
await asyncio.sleep(5) await asyncio.sleep(5)
except Exception as e:
self.log.error(f"Error in heartbeat: {e}")
async def main(self): async def main(self):
self.run = True
self.tcp = self.tcp(self, self.server_ip, self.server_port) self.tcp = self.tcp(self, self.server_ip, self.server_port)
self.udp = self.udp(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: try:
# WebApi Start # WebApi Start
if config.WebAPI["enabled"]: if config.WebAPI["enabled"]:
self.log.debug("Initializing WebAPI...") self.log.debug("Initializing WebAPI...")
web_thread = Thread(target=self.start_web) web_thread = Thread(target=self.start_web, name="WebApiThread")
web_thread.start() web_thread.start()
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
self.web_thread = web_thread self.web_thread = web_thread
# noinspection PyProtectedMember
self.web_stop = webapp._stop self.web_stop = webapp._stop
await asyncio.sleep(.3)
# Mods handler # Mods handler
self.log.debug("Listing mods..") self.log.debug("Listing mods..")
@@ -305,37 +240,49 @@ class Core:
self.mods_list.append({"path": path, "size": size}) self.mods_list.append({"path": path, "size": size})
self.mods_list[0] += size self.mods_list[0] += size
self.log.debug(f"mods_list: {self.mods_list}") self.log.debug(f"mods_list: {self.mods_list}")
lmods = len(self.mods_list) - 1 len_mods = len(self.mods_list) - 1
if lmods > 0: if len_mods > 0:
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb") # 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.authenticate(True) await self.heartbeat(True)
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
self.clients.append(None)
tasks = [] tasks = []
# self.check_alive() # self.udp.start,
nrtasks = [self.tcp.start, self.udp.start, console.start, self.stop_me, self.authenticate, ] f_tasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
for task in nrtasks: for task in f_tasks:
tasks.append(asyncio.create_task(task())) tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
await ev.call_async_event("_plugins_start")
self.run = True
self.log.info(i18n.start) self.log.info(i18n.start)
ev.call_event("on_started") ev.call_event("server_started")
await t await ev.call_async_event("server_started")
# Wait the end. await t # Wait end.
except KeyboardInterrupt:
pass
except Exception as e: except Exception as e:
self.log.error(f"Exception: {e}") self.log.error(f"Exception: {e}")
self.log.exception(e) self.log.exception(e)
except KeyboardInterrupt:
pass
finally: finally:
self.tcp.stop()
self.udp.stop()
self.run = False self.run = False
self.tcp.stop()
# self.udp.stop()
await self.stop()
def start(self): def start(self):
asyncio.run(self.main()) asyncio.run(self.main())
def stop(self): async def stop(self):
ev.call_event("server_stopped")
await ev.call_async_event("server_stopped")
await ev.call_async_event("_plugins_unload")
self.run = False self.run = False
self.log.info(i18n.stop) self.log.info(i18n.stop)
asyncio.run(self.web_stop()) if config.WebAPI["enabled"]:
exit(0) asyncio.run(self.web_stop())
# exit(0)

View File

@@ -1,50 +1,28 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.core.pyi # File core.core.pyi
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.2.0 # Version 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
from asyncio import StreamWriter, StreamReader
from threading import Thread from threading import Thread
from typing import Callable from typing import Callable, List, Dict
from core import utils from core import utils
from .Client import Client
from .tcp_server import TCPServer from .tcp_server import TCPServer
from .udp_server import UDPServer from .udp_server import UDPServer
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
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 = 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) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def last_handle(self) -> bytes: ...
def _update_logger(self) -> None: ...
class Core: class Core:
def __init__(self): def __init__(self):
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.run = False self.run = False
self.direct = False self.direct = False
self.clients = dict() self.clients: List[Client | None]= []
self.clients_by_id: Dict[{int: Client}]= {}
self.clients_by_nick: Dict[{str: Client}] = {}
self.clients_counter: int = 0 self.clients_counter: int = 0
self.mods_dir: str = "mods" self.mods_dir: str = "mods"
self.mods_list: list = [] self.mods_list: list = []
@@ -56,14 +34,15 @@ class Core:
self.web_stop: Callable = lambda: None self.web_stop: Callable = lambda: None
self.client_major_version = "2.0" self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0" self.BeamMP_version = "3.2.0"
def insert_client(self, client: Client) -> None: ... 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 create_client(self, *args, **kwargs) -> Client: ...
def get_clients_list(self, need_cid=False) -> str: ...
async def check_alive(self) -> None: ... async def check_alive(self) -> None: ...
@staticmethod @staticmethod
def start_web() -> None: ... def start_web() -> None: ...
@staticmethod def stop_me(self) -> None: ...
def stop_me() -> None: ... async def heartbeat(self, test=False) -> None: ...
async def authenticate(self, test=False) -> None: ...
async def main(self) -> None: ... async def main(self) -> None: ...
def start(self) -> None: ... def start(self) -> None: ...
def stop(self) -> None: ... async def stop(self) -> None: ...

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.2.0 # Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -15,84 +15,106 @@ from core import utils
class TCPServer: class TCPServer:
def __init__(self, core, host, port): def __init__(self, core, host, port):
self.log = utils.get_logger("TCPServer") self.log = utils.get_logger("TCPServer")
self.loop = asyncio.get_event_loop()
self.Core = core self.Core = core
self.host = host self.host = host
self.port = port self.port = port
self.loop = asyncio.get_event_loop() self.run = False
async def auth_client(self, reader, writer): async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer) client = self.Core.create_client(reader, writer)
# TODO: i18n
self.log.info(f"Identifying new ClientConnection...") self.log.info(f"Identifying new ClientConnection...")
data = await client.recv() data = await client._recv()
self.log.debug(f"recv1 data: {data}") self.log.debug(f"Version: {data}")
if len(data) > 50: if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
await client.kick("Too long data") # TODO: i18n
return False, None
if "VC2.0" not in data.decode("utf-8"):
await client.kick("Outdated Version.") await client.kick("Outdated Version.")
return False, None return False, client
else: else:
await client.tcp_send(b"S") # Accepted client version await client._send(b"S") # Accepted client version
data = await client.recv() data = await client._recv()
self.log.debug(f"recv2 data: {data}") self.log.debug(f"Key: {data}")
if len(data) > 50: if len(data) > 50:
# TODO: i18n
await client.kick("Invalid Key (too long)!") await client.kick("Invalid Key (too long)!")
return False, None return False, client
client.key = data.decode("utf-8") client._key = data.decode("utf-8")
async with aiohttp.ClientSession() as session: ev.call_event("auth_sent_key", player=client)
url = 'https://auth.beammp.com/pkToUser'
async with session.post(url, data={'key': client.key}) as response:
res = await response.json()
self.log.debug(f"res: {res}")
try: try:
async with aiohttp.ClientSession() as session:
url = 'https://auth.beammp.com/pkToUser'
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"): if res.get("error"):
# TODO: i18n
await client.kick('Invalid key! Please restart your game.') await client.kick('Invalid key! Please restart your game.')
return return False, client
client.nick = res["username"] client._nick = res["username"]
client.roles = res["roles"] client._roles = res["roles"]
client.guest = res["guest"] client._guest = res["guest"]
# noinspection PyProtectedMember
client._update_logger() client._update_logger()
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error(f"Auth error: {e}") self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to connect in 5 minutes.') await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
return False, client
# TODO: Password party for _client in self.Core.clients:
# await client.tcp_send(b"S") # Ask client key (How?) 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, client
ev.call_event("on_auth", client) ev.call_event("auth_ok", player=client)
if len(self.Core.clients) > config.Game["players"]: if len(self.Core.clients_by_id) > config.Game["players"]:
# TODO: i18n
await client.kick("Server full!") await client.kick("Server full!")
return False, client
else: else:
# TODO: i18n
self.log.info("Identification success") self.log.info("Identification success")
self.Core.insert_client(client) await self.Core.insert_client(client)
return True, client return True, client
async def handle_download(self, writer): async def set_down_rw(self, reader, writer):
# TODO: HandleDownload try:
self.log.debug(f"Client: \"IP: {0!r}; ID: {0}\" - HandleDownload!") cid = (await reader.read(1))[0]
return False client = self.Core.get_client(cid=cid)
if client:
client._down_rw = (reader, writer)
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
else:
writer.close()
self.log.debug(f"Unknown client id:{cid} - HandleDownload")
finally:
return
async def handle_code(self, code, reader, writer): async def handle_code(self, code, reader, writer):
match code: match code:
case "C": case "C":
result, client = await self.auth_client(reader, writer) result, client = await self.auth_client(reader, writer)
if result: if result:
await client.sync_resources() await client._looper()
# await client.kick("Authentication success! Server not ready.") return result, client
return True
return False
case "D": case "D":
return await self.handle_download(writer) await self.set_down_rw(reader, writer)
case "P": case "P":
writer.write(b"P") writer.write(b"P")
await writer.drain() await writer.drain()
return True writer.close()
case _: case _:
# TODO: i18n
self.log.error(f"Unknown code: {code}") self.log.error(f"Unknown code: {code}")
return False writer.close()
return False, None
async def handle_client(self, reader, writer): async def handle_client(self, reader, writer):
while True: while True:
@@ -102,28 +124,42 @@ class TCPServer:
break break
code = data.decode() code = data.decode()
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}") self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
result = await self.handle_code(code, reader, writer) # task = asyncio.create_task(self.handle_code(code, reader, writer))
if not result: # await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
break _, cl = await self.handle_code(code, reader, writer)
if cl:
await cl._remove_me()
del cl
break
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error("Error while connecting..") self.log.error("Error while connecting..")
self.log.error(f"Error: {e}") self.log.exception(e)
traceback.print_exc() traceback.print_exc()
break break
async def start(self): async def start(self):
self.log.debug("Starting TCP server.") self.log.debug("Starting TCP server.")
self.run = True
try: try:
server = await asyncio.start_server(self.handle_client, self.host, self.port, server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=config.Game["players"] + 1) backlog=int(config.Game["players"] * 1.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: except OSError as e:
self.log.error(f"Error: {e}") # TODO: i18n
self.Core.run = False self.log.error("Cannot bind port")
raise e raise e
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}") except KeyboardInterrupt:
while True: pass
async with server: except Exception as e:
await server.serve_forever() self.log.error(f"Error: {e}")
raise e
finally:
self.run = False
self.Core.run = False
def stop(self): def stop(self):
self.log.debug("Stopping TCP server") self.log.debug("Stopping TCP server")

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.pyi # File core.tcp_server.pyi
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.2.0 # Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -9,19 +9,20 @@ from asyncio import StreamWriter, StreamReader
from typing import Tuple from typing import Tuple
from core import utils, Core from core import utils, Core
from core.core import Client from core.Client import Client
class TCPServer: class TCPServer:
def __init__(self, core: Core, host, port): def __init__(self, core: Core, host, port):
self.log = utils.get_logger("TCPServer") self.log = utils.get_logger("TCPServer")
self.loop = asyncio.get_event_loop()
self.Core = core self.Core = core
self.host = host self.host = host
self.port = port self.port = port
self.loop = asyncio.get_event_loop() self.run = False
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ... async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
async def handle_download(self, writer: StreamWriter) -> bool: ... async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> bool: ... async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
async def handle_client(self, reader: StreamReader, writer: StreamWriter) -> None: ... async def handle_client(self, reader: StreamReader, writer: StreamWriter) -> None: ...
async def start(self) -> None: ... async def start(self) -> None: ...
async def stop(self) -> None: ... async def stop(self) -> None: ...

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.udp_server.py # File core.udp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.0 # Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -14,43 +14,47 @@ class UDPServer:
def __init__(self, core, host, port): def __init__(self, core, host, port):
self.log = utils.get_logger("UDPServer") self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core self.Core = core
self.host = host self.host = host
self.port = port self.port = port
self.loop = asyncio.get_event_loop() self.run = False
async def handle_client(self, srv_sock): async def handle_client(self, reader, writer):
while True: while True:
try: try:
data, addr = await self.loop.sock_recv(srv_sock, 1024) data = await reader.read(1)
if not data: if not data:
break break
code = data.decode() code = data.decode()
self.log.debug(f"Received {code!r} from {addr!r}") self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
# if not await self.handle_code(code, sock): # await self.handle_code(code, reader, writer)
# break # 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: except Exception as e:
self.log.error("Error while connecting..")
self.log.error(f"Error: {e}") self.log.error(f"Error: {e}")
traceback.print_exc() traceback.print_exc()
break break
srv_sock.close()
self.log.error("Error while connecting..")
async def start(self): async def start(self):
pass self.log.debug("Starting UDP server.")
# self.log.debug("Starting UDP server.") self.run = True
# await self.stop() try:
# srv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) pass
# srv_sock.bind((self.host, self.port)) except OSError as e:
# self.log.debug(f"Serving on {srv_sock.getsockname()}") self.log.error("Cannot bind port or other error")
# try: raise e
# await self.handle_client(srv_sock) except BaseException as e:
# except Exception as e: self.log.error(f"Error: {e}")
# self.log.error(f"Error: {e}") raise e
# traceback.print_exc() finally:
# finally: self.run = False
# await self.stop() self.Core.run = False
def stop(self): def stop(self):
pass self.log.debug("Stopping UDP server")
# self.log.debug("Stopping UDP server")

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.udp_server.py # File core.udp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.0 # Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -13,10 +13,11 @@ class UDPServer:
def __init__(self, core, host, port): def __init__(self, core, host, port):
self.log = utils.get_logger("UDPServer") self.log = utils.get_logger("UDPServer")
self.loop = asyncio.get_event_loop()
self.Core = core self.Core = core
self.host = host self.host = host
self.port = port self.port = port
self.loop = asyncio.get_event_loop() self.run = False
async def handle_client(self, srv_sock) -> None: ... async def handle_client(self, srv_sock) -> None: ...
async def start(self) -> None: ... async def start(self) -> None: ...

View File

@@ -1,21 +1,38 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.utils.py # File core.utils.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.0 # Version 1.1
# Core version: 0.2.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import datetime
import logging import logging
import os
import tarfile
log_format = "[%(asctime)s | %(name)-14s | %(levelname)-5s] %(message)s" log_format = "[%(asctime)s | %(name)-14s | %(levelname)-5s] %(message)s"
log_format_access = '[%(asctime)s | %(name)-14s | %(levelname)-5s] %(client_addr)s - "%(request_line)s" %(status_code)s' log_dir = "./logs/"
log_file = "server.log" log_file = log_dir + "server.log"
log_level = logging.INFO log_level = logging.INFO
# Инициализируем логирование # Инициализируем логирование
logging.basicConfig(level=log_level, format=log_format) logging.basicConfig(level=log_level, format=log_format)
# Настройка логирование в файл. # Настройка логирование в файл.
# if os.path.exists(log_file): if not os.path.exists(log_dir):
# os.remove(log_file) 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"
index = 1
while True:
if not os.path.exists(gz_path % index):
break
index += 1
with tarfile.open(gz_path % index, "w:gz") as tar:
logs_files = [log_file, "./logs/web.log", "./logs/web_access.log"]
for file in logs_files:
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)) fh.setFormatter(logging.Formatter(log_format))

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('--config', help='Patch to config file.', nargs='?', default=None, type=str)
parser.add_argument('--language', help='Setting localisation.', nargs='?', default=None, type=str) parser.add_argument('--language', help='Setting localisation.', nargs='?', default=None, type=str)
run = True
def main(): def main():
from core import Core from core import Core
core = Core()
try: try:
core.start() Core().start()
except KeyboardInterrupt: except KeyboardInterrupt:
core.run = False pass
core.stop()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.config_provider.py # File modules.ConsoleSystem.console_system.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.1 # Version 1.2
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import builtins import builtins
@@ -14,6 +14,7 @@ from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger from core import get_logger
@@ -25,6 +26,7 @@ class Console:
prompt_out="", prompt_out="",
not_found="Command \"%s\" not found in alias.", not_found="Command \"%s\" not found in alias.",
debug=False) -> None: debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False self.__is_run = False
self.__prompt_in = prompt_in self.__prompt_in = prompt_in
self.__prompt_out = prompt_out self.__prompt_out = prompt_out
@@ -43,10 +45,11 @@ class Console:
self.completer = NestedCompleter.from_nested_dict(self.__alias) self.completer = NestedCompleter.from_nested_dict(self.__alias)
def __debug(self, *x): def __debug(self, *x):
if self.__is_debug: self.__logger.debug(f"{x}")
x = list(x) # if self.__is_debug:
x.insert(0, "\r CONSOLE DEBUG:") # x = list(x)
self.__print(*x) # x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item): def __getitem__(self, item):
print(item) print(item)
@@ -84,7 +87,7 @@ class Console:
print() print()
raw = True raw = True
message = str() message = "\n"
max_len = self.__get_max_len(self.__func.keys()) max_len = self.__get_max_len(self.__func.keys())
if max_len < 7: if max_len < 7:
max_len = 7 max_len = 7
@@ -131,7 +134,8 @@ class Console:
print_formatted_text(s) print_formatted_text(s)
def log(self, s: AnyStr) -> None: def log(self, s: AnyStr) -> None:
self.write(s) self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None: def __lshift__(self, s: AnyStr) -> None:
self.write(s) self.write(s)
@@ -186,22 +190,29 @@ class Console:
session = PromptSession(history=FileHistory('./.cmdhistory')) session = PromptSession(history=FileHistory('./.cmdhistory'))
while True: while True:
try: try:
cmd_in = await session.prompt_async(self.__prompt_in, with patch_stdout():
completer=self.completer, auto_suggest=AutoSuggestFromHistory()) cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
cmd_s = cmd_in.split(" ") cmd_s = cmd_in.split(" ")
cmd = cmd_s[0] cmd = cmd_s[0]
if cmd == "": if cmd == "":
pass continue
else: else:
command_object = self.__func.get(cmd) command_object = self.__func.get(cmd)
if command_object: if command_object:
self.log(str(command_object['f'](cmd_s[1:]))) out = command_object['f'](cmd_s[1:])
if out:
self.log(out)
else: else:
self.log(self.__not_found % cmd) self.log(self.__not_found % cmd)
except KeyboardInterrupt: except KeyboardInterrupt:
raise KeyboardInterrupt raise KeyboardInterrupt
except Exception as e: except Exception as e:
print(f"Error in console.py: {e}") print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self): async def start(self):
self.__is_run = True self.__is_run = True
@@ -210,13 +221,3 @@ class Console:
def stop(self, *args, **kwargs): def stop(self, *args, **kwargs):
self.__is_run = False self.__is_run = False
raise KeyboardInterrupt raise KeyboardInterrupt
# if __name__ == '__main__':
# c = Console()
# c.logger_hook()
# c.builtins_hook()
# log = logging.getLogger(name="name")
# log.info("Starting console")
# print("Starting console")
# asyncio.run(c.start())

View File

@@ -1,46 +1,99 @@
# -*- 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 builtins
import inspect
from core import get_logger from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem: class EventsSystem:
def __init__(self): def __init__(self):
self.__events = { # TODO: default events
"on_started": [self.on_started],
"on_stop": [self.on_stop],
"on_auth": [self.on_auth]
}
self.log = get_logger("EventsSystem") self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"server_started": [],
"auth_sent_key": [], # Only sync
"auth_ok": [], # Only sync
"player_join": [],
"chat_receive": [],
"server_stopped": [],
}
self.__async_events = {
"server_started": [],
"_plugins_start": [],
"_plugins_unload": [],
"player_join": [],
"chat_receive": [],
"server_stopped": []
}
def builtins_hook(self): def builtins_hook(self):
self.log.debug("used builtins_hook") self.log.debug("used builtins_hook")
builtins.ev = self 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}):") self.log.debug(f"register_event({event_name}, {event_func}):")
if not callable(event_func): if not callable(event_func):
# TODO: i18n
self.log.error(f"Cannot add event '{event_name}'. " self.log.error(f"Cannot add event '{event_name}'. "
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...") f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return return
if event_name not in self.__events: if async_event or inspect.iscoroutinefunction(event_func):
self.__events.update({str(event_name): [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: 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):
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(): if event_name in self.__events.keys():
for event in self.__events[event_name]: for func in self.__events[event_name]:
event(*data) 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: 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): return funcs_data
pass
def on_stop(self):
pass
def on_auth(self, client):
pass

View File

@@ -1,6 +1,11 @@
from typing import Any
class EventsSystem: class EventsSystem:
@staticmethod @staticmethod
def register_event(self, event_name, event_func): ... def register_event(event_name, event_func): ...
@staticmethod @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): ... 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 os
import types import types
from contextlib import contextmanager
from threading import Thread
from core import get_logger from core import get_logger
# TODO: call_client_event, get_player, get_players, GetPlayerCount
class KuiToi: class KuiToi:
_plugins_dir = ""
def __init__(self, name=None): def __init__(self, name=None):
if name is None: if name is None:
raise Exception("BeamMP: Name is required") raise AttributeError("KuiToi: Name is required")
self.log = get_logger(f"PluginsLoader | {name}") self.log = get_logger(f"Plugin | {name}")
self.name = 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): @property
self.name = name def name(self):
return self.__name
@staticmethod @name.setter
def register_event(event_name, event_func): 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) ev.register_event(event_name, event_func)
@staticmethod def call_event(self, event_name, *data, **kwargs):
def call_event(event_name, *data): self.log.debug(f"Called event {event_name}")
ev.call_event(event_name, *data) ev.call_event(event_name, *data, **kwargs)
class PluginsLoader: class PluginsLoader:
def __init__(self, plugins_dir): def __init__(self, plugins_dir):
self.__plugins = {} self.loop = asyncio.get_event_loop()
self.__plugins_dir = plugins_dir self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader") 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...") self.log.debug("Loading plugins...")
files = os.listdir(self.__plugins_dir) files = os.listdir(self.plugins_dir)
for file in files: for file in files:
if file.endswith(".py"): if file.endswith(".py"):
try: try:
self.log.debug(f"Loading plugin: {file}") self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType('plugin') plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print plugin.print = print
file = os.path.join(self.__plugins_dir, file) file_path = os.path.join(self.plugins_dir, file)
with open(f'{file}', 'r') as f: plugin.__file__ = file_path
code = f.read().replace("import KuiToi\n", "") with open(f'{file_path}', 'r', encoding="utf-8") as f:
code = f.read()
exec(code, plugin.__dict__) exec(code, plugin.__dict__)
plugin.load()
self.__plugins.update({file[:-3]: plugin}) ok = True
self.log.debug(f"Plugin loaded: {file}") 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: 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

@@ -7,7 +7,6 @@ from fastapi.exceptions import RequestValidationError
from starlette import status from starlette import status
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from uvicorn.config import LOGGING_CONFIG
import core.utils import core.utils
from . import utils from . import utils
@@ -21,30 +20,6 @@ uvserver = None
data_pool = [] data_pool = []
data_run = [True] data_run = [True]
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format_access
LOGGING_CONFIG["formatters"].update({
"file_default": {
"fmt": core.utils.log_format
},
"file_access": {
"fmt": core.utils.log_format_access
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None): def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
if 200 >= code <= 300: if 200 >= code <= 300:
@@ -78,7 +53,8 @@ async def _method(method, secret_key: str = None):
async def _stop(): async def _stop():
await asyncio.sleep(1) await asyncio.sleep(1)
uvserver.should_exit = True if uvserver is not None:
uvserver.should_exit = True
data_run[0] = False data_run[0] = False

View File

@@ -2,10 +2,17 @@ import asyncio
import sys import sys
import click import click
from uvicorn.server import Server, logger import uvicorn.server as uvs
from uvicorn.config import LOGGING_CONFIG
from uvicorn.lifespan import on from uvicorn.lifespan import on
import core.utils
# logger = core.utils.get_logger("uvicorn")
# uvs.logger = logger
logger = uvs.logger
def ev_log_started_message(self, listeners) -> None: def ev_log_started_message(self, listeners) -> None:
cfg = self.config cfg = self.config
@@ -42,7 +49,7 @@ async def ev_shutdown(self, sockets=None) -> None:
try: try:
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown) await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded",len(self.server_state.tasks)) logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded", len(self.server_state.tasks))
for t in self.server_state.tasks: for t in self.server_state.tasks:
if sys.version_info < (3, 9): if sys.version_info < (3, 9):
t.cancel() t.cancel()
@@ -81,7 +88,39 @@ async def on_shutdown(self) -> None:
def hack_fastapi(): def hack_fastapi():
Server.shutdown = ev_shutdown uvs.Server.shutdown = ev_shutdown
Server._log_started_message = ev_log_started_message uvs.Server._log_started_message = ev_log_started_message
on.LifespanOn.startup = on_startup on.LifespanOn.startup = on_startup
on.LifespanOn.shutdown = on_shutdown on.LifespanOn.shutdown = on_shutdown
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format
LOGGING_CONFIG["formatters"].update({
"file_default": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
},
"file_access": {
"()": "logging.Formatter",
"fmt": core.utils.log_format
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web.log",
"encoding": "utf-8",
"formatter": "file_default"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "./logs/web_access.log",
"encoding": "utf-8",
"formatter": "file_access"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")

View File

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

View File

@@ -3,7 +3,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File modules.i18n.i18n.py # File modules.i18n.i18n.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.0 # Version 1.3
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import builtins import builtins
@@ -89,53 +89,53 @@ class MultiLanguage:
else: else:
# noinspection PyDictDuplicateKeys # noinspection PyDictDuplicateKeys
self.__data = { self.__data = {
"": "Basic phases", "": "Basic phases",
"hello": "Hello from KuiToi-Server!", "hello": "Greetings from KuiToi Server!",
"config_path": "Use {} for config.", "config_path": "Use {} to configure.",
"init_ok": "Initializing ready.", "init_ok": "Initialization complete.",
"start": "Server started!", "start": "Server started!",
"stop": "Goodbye!", "stop": "Server stopped!",
"": "Server auth", "": "Server auth",
"auth_need_key": "BEAM key needed for starting the server!", "auth_need_key": "A BeamMP key is required to start the server!",
"auth_empty_key": "Key is empty!", "auth_empty_key": "The BeamMP key is empty!",
"auth_cannot_open_browser": "Cannot open browser: {}", "auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}", "auth_use_link": "Use this link: {}",
"": "GUI phases", "": "GUI phases",
"GUI_yes": "Yes", "GUI_yes": "Yes",
"GUI_no": "No", "GUI_no": "No",
"GUI_ok": "Ok", "GUI_ok": "Ok",
"GUI_cancel": "Cancel", "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_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 type your key:", "GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}", "GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"": "Web phases", "": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)", "web_start": "WebAPI started at {} (Press CTRL+C to quit)",
"": "Command: man", "": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND", "man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.", "help_message_man": "Displays help page for COMMAND.",
"man_for": "Manual for command", "man_for": "Help page for",
"man_message_not_found": "man: Manual message not found.", "man_message_not_found": "man: Help page not found.",
"man_command_not_found": "man: command \"{}\" not found!", "man_command_not_found": "man: Command \"{}\" not found!",
"": "Command: help", "": "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.", "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": "Display names and brief descriptions of available commands", "help_message_help": "Displays the names and short descriptions of commands.",
"help_command": "Command", "help_command": "Command",
"help_message": "Help message", "help_message": "Description",
"help_message_not_found": "No help message found", "help_message_not_found": "No description available.",
"": "Command: stop", "": "Command: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop", "man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Server shutdown.", "help_message_stop": "Stops the server.",
"": "Command: exit", "": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop", "man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Server shutdown." "help_message_exit": "Stops the server."
} }
self.__i18n = i18n(self.__data) self.__i18n = i18n(self.__data)
def open_file(self): def open_file(self):