mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2026-02-16 10:30:57 +00:00
Compare commits
92 Commits
0.2.2-alph
...
0.4.1-beta
| Author | SHA1 | Date | |
|---|---|---|---|
| 8139cbf8bc | |||
| eec7c8129d | |||
| 158599dfc5 | |||
| 06bd50f0fa | |||
| e086fea2e9 | |||
| b6038ee6d0 | |||
| 147e76e089 | |||
| 56b9049dcb | |||
| 78d323644d | |||
| 310c47162c | |||
| 27d49cf5cc | |||
| a5a7a5dfc9 | |||
| f6ff018b03 | |||
| 1829113ae5 | |||
| e72c371e20 | |||
| 57b7cebeca | |||
| 2a2d55946e | |||
| ea2d715cae | |||
| 102891c8e8 | |||
| 46b0419340 | |||
| 47cca3a0d8 | |||
| 77ee76c0c0 | |||
| 852e977a75 | |||
| 407127ec97 | |||
| 7dd3faac12 | |||
| ef69df10d6 | |||
| a226b17612 | |||
| 69348e9339 | |||
| 31d8cf7842 | |||
| 45d45a820c | |||
| aa440a1e3d | |||
| 63c9515e86 | |||
| cfeb2e9823 | |||
| 85b85114b5 | |||
| 792884d7b0 | |||
| a5b087f8b4 | |||
| a01567c89a | |||
| 041883644c | |||
| 3d33eec5fd | |||
| 3f2c5b24f9 | |||
| b7ea7ff362 | |||
| 07ec15170b | |||
| eb88af247c | |||
| 6dedf518e2 | |||
| 69ee180128 | |||
| 98f86b2248 | |||
| 98ef332193 | |||
| 642c91d59c | |||
| acdb32d900 | |||
| 50b1e7b176 | |||
| c9e6a0a9cd | |||
| cd098571d9 | |||
| a73b14f9b4 | |||
| e3e5c6ecbb | |||
| 5953923368 | |||
| 580b836e39 | |||
| 4974d48411 | |||
| 3b7842aa50 | |||
| db6eb361b8 | |||
| 479525a66e | |||
| 6d4bc1e72c | |||
| 9b3677de46 | |||
| 58137752c5 | |||
| 220c6068e4 | |||
| a9dad5ab8f | |||
| aa5725e8a5 | |||
| 939723acdd | |||
| 90beaf1302 | |||
| ee366a2d23 | |||
| d665021479 | |||
| 13ff3207b2 | |||
| 50b479c396 | |||
| 62fa4c6f25 | |||
| f0f8da962e | |||
| 840d8fd685 | |||
| 4629fbc43a | |||
| e9919459af | |||
| 5f8b70a2ee | |||
| a66f3d8b36 | |||
| 4c3da30a94 | |||
| 9c52e41b99 | |||
| 51f960f7c2 | |||
| 0cbed05d68 | |||
| c6c6ec31b0 | |||
| 8feba0e085 | |||
| a5202edf83 | |||
| 64ce662d04 | |||
| 08e4f0fcba | |||
| 99f40eadb0 | |||
| 5a40ab8b05 | |||
| 4f688d7c02 | |||
| c4fe201b86 |
2
LICENCE
2
LICENCE
@@ -6,6 +6,6 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
||||
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
|
||||
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
51
README.md
51
README.md
@@ -1,59 +1,72 @@
|
||||
# KuiToi-Server
|
||||
|
||||
## About
|
||||
**_[Status: Alpha]_** \
|
||||
**_[Status: Beta]_** \
|
||||
BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Server core
|
||||
- [x] Server core
|
||||
- [x] BeamMP System
|
||||
- [x] Private access (Without key, Direct connect)
|
||||
- [x] Public access (With key, listing in Launcher)
|
||||
- [X] Player authentication
|
||||
- [ ] KuiToi System
|
||||
- [ ] Servers counter
|
||||
- [ ] Players counter
|
||||
- [ ] Etc.
|
||||
- [ ] TCP Server part:
|
||||
- [x] TCP Server part:
|
||||
- [x] Handle code
|
||||
- [x] Understanding BeamMP header
|
||||
- [x] Upload mods
|
||||
- [x] Connecting to the world
|
||||
- [x] Chat
|
||||
- [x] Players online counter
|
||||
- [ ] Car state synchronizations _(Codes: We, Vi)_
|
||||
- [ ] "ABG:" (compressed data)
|
||||
- [x] Packets handled (Recursive finding second packet)
|
||||
- [ ] Client events
|
||||
- [x] Car synchronizations:
|
||||
- [x] State packets
|
||||
- [x] Spawn cars
|
||||
- [x] Delete cars
|
||||
- [x] Edit cars
|
||||
- [x] Reset cars
|
||||
- [x] "ABG:" (compressed data)
|
||||
- [x] Decompress data
|
||||
- [ ] Vehicle data _(Code: Os)_
|
||||
- [ ] UDP Server part:
|
||||
- [ ] Players synchronizations _(Code: Zp)_
|
||||
- [ ] Ping _(Code: p)_
|
||||
- [x] Compress data
|
||||
- [x] UDP Server part:
|
||||
- [x] Ping
|
||||
- [x] Position synchronizations
|
||||
- [x] Additional:
|
||||
- [ ] KuiToi System
|
||||
- [ ] Servers counter
|
||||
- [ ] Players counter
|
||||
- [ ] Etc.
|
||||
- [x] Logger
|
||||
- [x] Just logging
|
||||
- [x] Log in file
|
||||
- [x] Log history (.1.log, .2.log, ...)
|
||||
- [x] Console:
|
||||
- [x] Tabulation
|
||||
- [ ] _(Deferred)_ Static text (bug)
|
||||
- [x] History
|
||||
- [x] Autocomplete
|
||||
- [x] Events System
|
||||
- [x] Call events
|
||||
- [x] Create custom events
|
||||
- [ ] Return from events
|
||||
- [x] Return from events
|
||||
- [x] Async support
|
||||
- [ ] Add all events
|
||||
- [x] Plugins support
|
||||
- [ ] KuiToi class
|
||||
- [ ] Client (Player) class
|
||||
- [x] Load Python plugins
|
||||
- [x] Async support
|
||||
- [ ] Load Lua plugins (Original BeamMP compatibility)
|
||||
- [x] MultiLanguage (i18n support)
|
||||
- [x] Core
|
||||
- [ ] Core
|
||||
- [x] Console
|
||||
- [x] WebAPI
|
||||
- [x] HTTP API Server (fastapi)
|
||||
- [ ] HTTP API Server (fastapi)
|
||||
- [x] Stop and Start with core
|
||||
- [x] Configure FastAPI logger
|
||||
- [ ] Sync with event system
|
||||
- [ ] Add methods...
|
||||
- [ ] [Documentation](docs/en/readme.md)
|
||||
- [ ] [Documentation](./docs/)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -78,6 +91,6 @@ Copyright (c) 2023 SantaSpeen (Maxim Khomutov)
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
|
||||
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
|
||||
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
||||
"auth_empty_key": "The BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
@@ -17,32 +17,32 @@
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
|
||||
"GUI_enter_key_message": "Please enter the key:",
|
||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
|
||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Displays help page for COMMAND.",
|
||||
"man_for": "Help page for",
|
||||
"man_message_not_found": "man: Help page not found.",
|
||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
|
||||
"help_message_help": "Displays the names and short descriptions of commands.",
|
||||
"help_command": "Command",
|
||||
"help_message": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"help_message": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
37
docs/en/plugins/async_example.py
Normal file
37
docs/en/plugins/async_example.py
Normal 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("Плагин выгружен успешно.")
|
||||
@@ -1,36 +1,37 @@
|
||||
import KuiToi # Import server object
|
||||
import json
|
||||
|
||||
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
|
||||
log = beam.log # Use logger from server
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
def on_load():
|
||||
# When plugin initialization Server uses plugin.load() to load plugin.
|
||||
# def load(): is really needed
|
||||
log.info(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
# Events handlers
|
||||
|
||||
def on_started():
|
||||
# Simple event handler
|
||||
log.info("Server starting...")
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
# Simple event register
|
||||
beam.register_event("on_started", on_started)
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def any_func(data=None):
|
||||
# Custom event handler
|
||||
log.info(f"Data from any_func: {data}")
|
||||
|
||||
|
||||
# Create custom event
|
||||
beam.register_event("my_event", any_func)
|
||||
|
||||
# Call custom event
|
||||
beam.call_event("my_event")
|
||||
beam.call_event("my_event", "Some data")
|
||||
# This will be an error since any_func accepts only one argument at the input
|
||||
beam.call_event("my_event", "Some data", "Some data1")
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
|
||||
@@ -1,32 +1,92 @@
|
||||
# Plugins System
|
||||
# Plugin System
|
||||
|
||||
## Install
|
||||
###### (Lib can't ready to use)
|
||||
## Installing the Library with "Stubs"
|
||||
###### (This means that it will not work without a server, but the IDE will suggest the API)
|
||||
###### (The library is still under development)
|
||||
|
||||
* From pip:\
|
||||
* Using pip:\
|
||||
`$ pip install KuiToi`
|
||||
* From source:\
|
||||
* From source code:\
|
||||
`git clone https://github.com/KuiToi/KuiToi-PyLib`
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
import KuiToi
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
beam = KuiToi("TestPlugin")
|
||||
logger = beam.log
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
def load(): # Plugins load from here
|
||||
print(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
def on_started():
|
||||
logger.info("Server starting...")
|
||||
def load():
|
||||
# Plugin initialization
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
beam.register_event("on_started", on_started)
|
||||
|
||||
def start():
|
||||
# Running plugin processes
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Plugin started successfully.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Code that ends all processes
|
||||
log.info("Plugin unloaded successfully.")
|
||||
```
|
||||
|
||||
* Basic Events: ['on_started', 'on_auth, 'on_stop']
|
||||
* Create new event : `beam.register_event("my_event", my_event_function)`
|
||||
* Call event: `beam.call_event("my_event")`
|
||||
* Call event with some data: `beam.call_event("my_event", data, data2)`
|
||||
* Calls _**can't support**_ like this: `beam.call_event("my_event", data=data)`
|
||||
* It is recommended to use `open()` after `load()`. Otherwise, use `kt.load()` - creates a file in the `plugin/<plugin_name>/<filename>` folder.
|
||||
* Creating your own event: `kt.register_event("my_event", my_event_function)`
|
||||
* Calling an event: `kt.call_event("my_event")`
|
||||
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
|
||||
* Basic events: _Will write later_
|
||||
|
||||
## Async Functions
|
||||
|
||||
Async support is available.
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Plugin initialization
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Plugin loaded successfully.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Running plugin processes
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Plugin started successfully.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Code that ends all processes
|
||||
log.info("Plugin unloaded successfully.")
|
||||
|
||||
```
|
||||
|
||||
A more extensive example can also be found in [async_example.py](./async_example.py).
|
||||
|
||||
* Creating your own event: `kt.register_event("my_event", my_event_function)` (register_event checks for function)
|
||||
* Calling an async event: `kt.call_async_event("my_event")`
|
||||
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
|
||||
* Basic async events: _Will write later_
|
||||
@@ -1,9 +1,9 @@
|
||||
# Documentation for KuiToi Server
|
||||
|
||||
#### The documentation has not been perfected yet, but one day it will definitely happen
|
||||
|
||||
### The documentation is not perfect yet, but it will be one day
|
||||
|
||||
1. Setup and Start server - [here](setup)
|
||||
2. Plugins and Events system - [here](plugins)
|
||||
3. MultiLanguage - [here](./multilanguage)
|
||||
4. RESP API - [here](./web)
|
||||
5. Something new
|
||||
4. KuiToi WebAPI - [here](./web)
|
||||
5. Something new...
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
# Hello from KuiToi Server
|
||||
# Greetings from KuiToi Server
|
||||
|
||||
## Start
|
||||
## Well, let's begin
|
||||
|
||||
* Need **Python 3.10.x** to start!
|
||||
* After cloning use this:
|
||||
###### _(Here are the commands for Linux)_
|
||||
|
||||
* **Python 3.10.x** is required to run the server! It won't work on Python 3.11...
|
||||
* You can check the version of your Python installation with the following command:
|
||||
```bash
|
||||
$ python3 --version # Python 3.10.6
|
||||
$ python3 main.py --help # Show help message
|
||||
$ python3 main.py # Start server
|
||||
python3 --version # Python 3.10.6
|
||||
```
|
||||
* Clone the repository and navigate to it.
|
||||
* Install everything that's needed.
|
||||
* Then, using my "script", remove all unnecessary files and move to the core source code.
|
||||
```bash
|
||||
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
|
||||
pip install -r requirements.txt
|
||||
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
|
||||
```
|
||||
* Here's how to view information about the server and start it:
|
||||
```bash
|
||||
python3 main.py --help # Displays all available commands
|
||||
python3 main.py # Starts the server
|
||||
```
|
||||
|
||||
## Setup
|
||||
## Configuration
|
||||
|
||||
* After starting server creating `kuitoi.yaml`; Default:
|
||||
* After starting the server, a `kuitoi.yaml` file will be created.
|
||||
* By default, it looks like this:
|
||||
```yaml
|
||||
!!python/object:modules.ConfigProvider.config_provider.Config
|
||||
Auth:
|
||||
@@ -23,11 +37,39 @@ Game:
|
||||
max_cars: 1
|
||||
players: 8
|
||||
Server:
|
||||
debug: true
|
||||
description: This server uses KuiToi!
|
||||
debug: false
|
||||
description: Welcome to KuiToi Server!
|
||||
language: en
|
||||
name: KuiToi-Server
|
||||
server_ip: 0.0.0.0
|
||||
server_port: 30814
|
||||
```
|
||||
* Server can't start without BEAM Auth.key
|
||||
server_port: 30813
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: <random_key>
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
|
||||
```
|
||||
### Auth
|
||||
|
||||
* If you set `private: false` and do not set a `key`, the server will request a BeamMP key and will not start without it.
|
||||
* By entering a BeamMP key, the server will appear in the launcher list.
|
||||
* You can get a key here: [https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
|
||||
|
||||
### Game
|
||||
|
||||
* `map` specifies only the name of the map. That is, open the mod with the map in `map.zip/levels` - the name of the map will be there, and that's what you need to insert.
|
||||
* `max_cars` - the maximum number of cars per player
|
||||
* `players` - the maximum number of players
|
||||
|
||||
### Server
|
||||
|
||||
* `debug` - should debug messages be displayed (for experienced users only; slightly affects performance)
|
||||
* `description` - server description for the BeamMP launcher
|
||||
* `language` - the language in which the server will run (currently available: en, ru)
|
||||
* `name` - server name for the BeamMP launcher
|
||||
* `server_ip` - the IP address to be used by the server (for experienced users only; defaults to 0.0.0.0)
|
||||
* `server_port` - the port on which the server will run
|
||||
|
||||
### WebAPI
|
||||
##### _Docs are not ready yet_
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# MultiLanguage - Поддержка i18n
|
||||
|
||||
В [example.json](./example.json) это копия [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json)\
|
||||
Если есть желание перевести на не переведённый ранее язык, или обновить уже существующий перевод буду рад вашим пул реквестам.
|
||||
|
||||
37
docs/ru/plugins/async_example.py
Normal file
37
docs/ru/plugins/async_example.py
Normal 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("Плагин выгружен успешно.")
|
||||
@@ -1,36 +1,37 @@
|
||||
import KuiToi # Import server object
|
||||
import json
|
||||
|
||||
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
|
||||
log = beam.log # Use logger from server
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
config = {"config_version": 0.1, "sql": {"enabled": False, "host": "127.0.0.1", "port": 3363, "database": "fucklua"}}
|
||||
cfg_file = "config.json"
|
||||
|
||||
|
||||
def on_load():
|
||||
# When plugin initialization Server uses plugin.load() to load plugin.
|
||||
# def load(): is really needed
|
||||
log.info(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
# Events handlers
|
||||
|
||||
def on_started():
|
||||
# Simple event handler
|
||||
log.info("Server starting...")
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
with open(cfg_file, 'w') as f:
|
||||
json.dump(config, f)
|
||||
cgf = config
|
||||
log.info(cgf)
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
# Simple event register
|
||||
beam.register_event("on_started", on_started)
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def any_func(data=None):
|
||||
# Custom event handler
|
||||
log.info(f"Data from any_func: {data}")
|
||||
|
||||
|
||||
# Create custom event
|
||||
beam.register_event("my_event", any_func)
|
||||
|
||||
# Call custom event
|
||||
beam.call_event("my_event")
|
||||
beam.call_event("my_event", "Some data")
|
||||
# This will be an error since any_func accepts only one argument at the input
|
||||
beam.call_event("my_event", "Some data", "Some data1")
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
|
||||
@@ -12,24 +12,81 @@
|
||||
## Пример
|
||||
|
||||
```python
|
||||
import KuiToi
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
beam = KuiToi("TestPlugin")
|
||||
logger = beam.log
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
def load(): # Plugins load from here
|
||||
print(beam.name)
|
||||
def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
def on_started():
|
||||
logger.info("Server starting...")
|
||||
def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
beam.register_event("on_started", on_started)
|
||||
|
||||
def start():
|
||||
# Запуск процессов плагина
|
||||
ev.call_event("my_event")
|
||||
ev.call_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
```
|
||||
|
||||
Так же более обширный пример можно найти в [example.py](./example.py)
|
||||
* Рекомендуется использовать `open()` после `load()`, иначе стоит использовать `kt.load()` - Создаёт файл в папке `plugin/<plugin_name>/<filename>`
|
||||
* Создание своего ивента : `kt.register_event("my_event", my_event_function)` -
|
||||
* Вызов ивента: `kt.call_event("my_event")`
|
||||
* Вызов ивента с данными: `kt.call_event("my_event", data, data2=data2)`
|
||||
* Базовые ивенты: _Позже напишу_
|
||||
|
||||
* Базовые ивенты: ['on_started', 'on_auth, 'on_stop']
|
||||
* Создание своего ивента : `beam.register_event("my_event", my_event_function)`
|
||||
* Вызов ивента: `beam.call_event("my_event")`
|
||||
* Вызов ивента с данными: `beam.call_event("my_event", data, data2)`
|
||||
* Вызовы с указанием переменой _**не поддерживаются**_: `beam.call_event("my_event", data=data)`
|
||||
## Async функции
|
||||
|
||||
Поддержка async есть
|
||||
|
||||
```python
|
||||
try:
|
||||
import KuiToi
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
kt = KuiToi("Example")
|
||||
log = kt.log
|
||||
|
||||
|
||||
async def my_event_handler(event_data):
|
||||
log.info(f"{event_data}")
|
||||
|
||||
|
||||
async def load():
|
||||
# Инициализация плагина
|
||||
ev.register_event("my_event", my_event_handler)
|
||||
log.info("Плагин загружен успешно.")
|
||||
|
||||
|
||||
async def start():
|
||||
# Запуск процессов плагина
|
||||
await ev.call_async_event("my_event")
|
||||
await ev.call_async_event("my_event", "Some data", data="some data too")
|
||||
log.info("Плагин запустился успешно.")
|
||||
|
||||
|
||||
async def unload():
|
||||
# Код завершающий все процессы
|
||||
log.info("Плагин выгружен успешно.")
|
||||
|
||||
```
|
||||
|
||||
Так же более обширный пример можно найти в [async_example.py](./async_example.py)
|
||||
|
||||
* Создание своего ивента: `kt.register_event("my_event", my_event_function)` (в register_event стоит проверка на функцию)
|
||||
* Вызов async ивента: `kt.call_async_event("my_event")`
|
||||
* Вызов async ивента: `kt.call_async_event("my_event", data, data2=data2)`
|
||||
* Базовые async ивенты: _Позже напишу_
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
1. Настройка и Запуск сервера - [тута](./setup)
|
||||
2. Плагины и Ивент система - [тута](./plugins)
|
||||
3. Мультиязычность - [тута](./multilanguage)
|
||||
4. RESP API - [тута](./web)
|
||||
4. KuiToi WebAPI - [тута](./web)
|
||||
5. Тут будет что-то ново....
|
||||
|
||||
@@ -38,12 +38,39 @@ Game:
|
||||
players: 8
|
||||
Server:
|
||||
debug: false
|
||||
description: This server uses KuiToi!
|
||||
description: Welcome to KuiToi Server!
|
||||
language: en
|
||||
name: KuiToi-Server
|
||||
server_ip: 0.0.0.0
|
||||
server_port: 30814
|
||||
server_port: 30813
|
||||
WebAPI:
|
||||
enabled: false
|
||||
secret_key: <random_key>
|
||||
server_ip: 127.0.0.1
|
||||
server_port: 8433
|
||||
|
||||
```
|
||||
### Auth
|
||||
|
||||
* Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
|
||||
* Введя BeamMP ключ сервер появится в списке лаунчера.
|
||||
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys)
|
||||
|
||||
### Game
|
||||
|
||||
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
|
||||
* `max_cars` - Максимальное количество машин на игрока
|
||||
* `players` - Максимально количество игроков
|
||||
|
||||
### Server
|
||||
|
||||
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
|
||||
* `description` - Описания сервера для лаунчера BeamMP
|
||||
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
|
||||
* `name` - Названия сервер для лаунчера BeamMP
|
||||
* `server_ip` - Какой IP адрес занять серверу (только для опытных пользователей, по умолчанию 0.0.0.0)
|
||||
* `server_port` - На каком порту будет работать сервер
|
||||
|
||||
### WebAPI
|
||||
##### _Доки не готовы_
|
||||
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
# Web RESP API для сервера
|
||||
# WebAPI для сервера
|
||||
|
||||
## Доступные endpoints
|
||||
|
||||
* `/stop`:
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
|
||||
* `/event.get`
|
||||
* Точка не готова
|
||||
* Необходимые парамеры:
|
||||
* `secret_key` - Ключ, который указан в конфигурации сервера
|
||||
|
||||
В разработке
|
||||
627
src/core/Client.py
Normal file
627
src/core/Client.py
Normal file
@@ -0,0 +1,627 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import zlib
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader, writer, core):
|
||||
self.__reader = reader
|
||||
self.__writer = writer
|
||||
self.__Core = core
|
||||
self.__alive = True
|
||||
self.__packets_queue = []
|
||||
self.__tasks = []
|
||||
self._down_sock = (None, None)
|
||||
self._udp_sock = (None, None)
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self._log = utils.get_logger("player(None:0)")
|
||||
self._addr = writer.get_extra_info("sockname")
|
||||
self._cid = -1
|
||||
self._key = None
|
||||
self.nick = None
|
||||
self.roles = None
|
||||
self._guest = True
|
||||
self._ready = False
|
||||
self._cars = [None] * 21 # Max 20 cars per player + 1 snowman
|
||||
self._snowman = {"id": -1, "packet": ""}
|
||||
self._connect_time = 0
|
||||
|
||||
@property
|
||||
def _writer(self):
|
||||
return self.__writer
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def addr(self):
|
||||
return self._addr
|
||||
|
||||
@property
|
||||
def cid(self):
|
||||
return self._cid
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def guest(self):
|
||||
return self._guest
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return self._ready
|
||||
|
||||
@property
|
||||
def cars(self):
|
||||
return self._cars
|
||||
|
||||
def _update_logger(self):
|
||||
self._log = utils.get_logger(f"{self.nick}:{self.cid}")
|
||||
self.log.debug(f"Update logger")
|
||||
|
||||
def is_disconnected(self):
|
||||
if not self.__alive:
|
||||
return True
|
||||
if self.__writer.is_closing():
|
||||
self.log.debug(f"is_d: Disconnected.")
|
||||
self.__alive = False
|
||||
return True
|
||||
else:
|
||||
self.__alive = True
|
||||
return False
|
||||
|
||||
async def kick(self, reason):
|
||||
if not self.__alive:
|
||||
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
|
||||
return
|
||||
# TODO: i18n
|
||||
self.log.info(f"Kicked with reason: \"{reason}\"")
|
||||
await self._send(f"K{reason}")
|
||||
self.__alive = False
|
||||
|
||||
async def send_message(self, message, to_all=True):
|
||||
pass
|
||||
|
||||
async def send_event(self, event_name, event_data):
|
||||
pass
|
||||
|
||||
async def _send(self, data, to_all=False, to_self=True, to_udp=False, writer=None):
|
||||
|
||||
# TNetwork.cpp; Line: 383
|
||||
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
|
||||
# [][][][][][]...[]
|
||||
# ^------^^---...-^
|
||||
# size data
|
||||
|
||||
if type(data) == str:
|
||||
data = bytes(data, config.enc)
|
||||
|
||||
if to_all:
|
||||
code = chr(data[0])
|
||||
for client in self.__Core.clients:
|
||||
if not client or (client is self and not to_self):
|
||||
continue
|
||||
if not to_udp or code in ['V', 'W', 'Y', 'E']:
|
||||
await client._send(data)
|
||||
else:
|
||||
await client._send(data, to_udp=to_udp)
|
||||
return
|
||||
|
||||
if not self.__alive:
|
||||
return False
|
||||
|
||||
if writer is None:
|
||||
writer = self.__writer
|
||||
|
||||
if len(data) > 400:
|
||||
data = b"ABG:" + zlib.compress(data, level=zlib.Z_BEST_COMPRESSION)
|
||||
|
||||
if to_udp:
|
||||
udp_sock = self._udp_sock[0]
|
||||
udp_addr = self._udp_sock[1]
|
||||
# self.log.debug(f'[UDP] len: {len(data)}; send: {data!r}')
|
||||
if udp_sock and udp_addr:
|
||||
try:
|
||||
if not udp_sock.is_closing():
|
||||
# self.log.debug(f'[UDP] {data!r}')
|
||||
udp_sock.sendto(data, udp_addr)
|
||||
except OSError:
|
||||
self.log.debug("[UDP] Error sending")
|
||||
except Exception as e:
|
||||
self.log.debug(f"[UDP] Error sending: {e}")
|
||||
self.log.exception(e)
|
||||
return
|
||||
|
||||
header = len(data).to_bytes(4, "little", signed=True)
|
||||
# self.log.debug(f'[TCP] {header + data!r}')
|
||||
try:
|
||||
writer.write(header + data)
|
||||
await writer.drain()
|
||||
return True
|
||||
|
||||
except ConnectionError:
|
||||
self.log.debug('[TCP] Disconnected')
|
||||
self.__alive = False
|
||||
await self._remove_me()
|
||||
return False
|
||||
|
||||
async def _recv(self, one=False):
|
||||
while self.__alive:
|
||||
try:
|
||||
header = await self.__reader.read(4)
|
||||
|
||||
int_header = int.from_bytes(header, byteorder='little', signed=True)
|
||||
|
||||
if int_header <= 0:
|
||||
await asyncio.sleep(0.1)
|
||||
self.is_disconnected()
|
||||
if self.__alive:
|
||||
if header == b"":
|
||||
self.__packets_queue.append(None)
|
||||
self.__alive = False
|
||||
continue
|
||||
self.log.error(f"Header: {header}")
|
||||
await self.kick("Invalid packet - header negative")
|
||||
self.__packets_queue.append(None)
|
||||
continue
|
||||
|
||||
if int_header > 100 * MB:
|
||||
await self.kick("Header size limit exceeded")
|
||||
self.log.warning("Client sent header of >100MB - "
|
||||
"assuming malicious intent and disconnecting the client.")
|
||||
self.log.error(f"Last recv: {await self.__reader.read(100 * MB)}")
|
||||
self.__packets_queue.append(None)
|
||||
continue
|
||||
|
||||
data = await self.__reader.read(int_header)
|
||||
|
||||
# self.log.debug(f"int_header: {int_header}; data: `{data}`;")
|
||||
abg = b"ABG:"
|
||||
if len(data) > len(abg) and data.startswith(abg):
|
||||
data = zlib.decompress(data[len(abg):])
|
||||
# self.log.debug(f"ABG Packet: {len(data)}")
|
||||
|
||||
if one:
|
||||
return data
|
||||
self.__packets_queue.append(data)
|
||||
|
||||
except ConnectionError:
|
||||
self.__alive = False
|
||||
self.__packets_queue.append(None)
|
||||
|
||||
async def _split_load(self, start, end, d_sock, filename, speed_limit=None):
|
||||
real_size = end - start
|
||||
writer = self._down_sock[1] if d_sock else self.__writer
|
||||
who = 'dwn' if d_sock else 'srv'
|
||||
self.log.debug(f"[{who}] Real size: {real_size / MB}mb; {real_size == end}, {real_size * 2 == end}")
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
f.seek(start)
|
||||
total_sent = 0
|
||||
start_time = time.monotonic()
|
||||
while total_sent < real_size:
|
||||
data = f.read(min(MB, real_size - total_sent)) # read data in chunks of 1MB or less
|
||||
try:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
self.log.debug(f"[{who}] Sent {len(data)} bytes.")
|
||||
except ConnectionError:
|
||||
self.__alive = False
|
||||
self.log.debug(f"[{who}] Disconnected.")
|
||||
break
|
||||
total_sent += len(data)
|
||||
|
||||
# Calculate delay based on speed limit
|
||||
if speed_limit:
|
||||
elapsed_time = time.monotonic() - start_time
|
||||
expected_time = total_sent / (speed_limit * MB)
|
||||
if expected_time > elapsed_time:
|
||||
await asyncio.sleep(expected_time - elapsed_time)
|
||||
|
||||
return total_sent
|
||||
|
||||
async def _sync_resources(self):
|
||||
while self.__alive:
|
||||
data = await self._recv(True)
|
||||
if data.startswith(b"f"):
|
||||
file = data[1:].decode(config.enc)
|
||||
# TODO: i18n
|
||||
self.log.info(f"Requested mode: {file!r}")
|
||||
size = -1
|
||||
for mod in self.__Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
if mod.get('path') == file:
|
||||
size = mod['size']
|
||||
self.log.debug("File is accept.")
|
||||
break
|
||||
self.log.debug(f"Mode size: {size}")
|
||||
if size == -1:
|
||||
await self._send(b"CO")
|
||||
await self.kick(f"Not allowed mod: " + file)
|
||||
return
|
||||
await self._send(b"AG")
|
||||
t = 0
|
||||
while not self._down_sock[0]:
|
||||
await asyncio.sleep(0.1)
|
||||
t += 1
|
||||
if t > 50:
|
||||
await self.kick("Missing download socket")
|
||||
return
|
||||
if config.Options['use_queue']:
|
||||
while self.__Core.lock_upload:
|
||||
await asyncio.sleep(.2)
|
||||
self.__Core.lock_upload = True
|
||||
speed = config.Options["speed_limit"]
|
||||
if speed:
|
||||
speed = speed / 2
|
||||
half_size = math.floor(size / 2)
|
||||
t = time.monotonic()
|
||||
uploads = [
|
||||
self._split_load(0, half_size, False, file, speed),
|
||||
self._split_load(half_size, size, True, file, speed)
|
||||
]
|
||||
sl0, sl1 = await asyncio.gather(*uploads)
|
||||
tr = time.monotonic() - t
|
||||
if self.__Core.lock_upload:
|
||||
self.__Core.lock_upload = False
|
||||
# TODO: i18n
|
||||
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
|
||||
if speed:
|
||||
msg += f" of limit {int(speed * 2)}Mb/s"
|
||||
self.log.info(msg)
|
||||
sent = sl0 + sl1
|
||||
ok = sent == size
|
||||
lost = size - sent
|
||||
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
||||
if not ok:
|
||||
self.__alive = False
|
||||
# TODO: i18n
|
||||
self.log.error(f"Error while sending: {file!r}")
|
||||
return
|
||||
elif data.startswith(b"SR"):
|
||||
path_list = ''
|
||||
size_list = ''
|
||||
for mod in self.__Core.mods_list:
|
||||
if type(mod) == int:
|
||||
continue
|
||||
path_list += f"{mod['path']};"
|
||||
size_list += f"{mod['size']};"
|
||||
mod_list = path_list + size_list
|
||||
self.log.debug(f"Mods List: {mod_list}")
|
||||
if len(mod_list) == 0:
|
||||
await self._send(b"-")
|
||||
else:
|
||||
await self._send(mod_list)
|
||||
elif data == b"Done":
|
||||
await self._send(f"M/levels/{config.Game['map']}/info.json")
|
||||
break
|
||||
return
|
||||
|
||||
def _get_cid_vid(self, data: str):
|
||||
sep = data.find(":", 1) + 1
|
||||
s = data[sep:sep + 3]
|
||||
id_sep = s.find('-')
|
||||
if id_sep == -1:
|
||||
self.log.debug(
|
||||
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'")
|
||||
return -1, -1
|
||||
cid = s[:id_sep]
|
||||
vid = s[id_sep + 1:]
|
||||
if cid.isdigit() and vid.isdigit():
|
||||
try:
|
||||
cid = int(cid)
|
||||
vid = int(vid)
|
||||
return cid, vid
|
||||
except ValueError:
|
||||
self.log.debug(f"Invalid packet: Could not parse cid/vid from packet, as one or both are not valid "
|
||||
f"numbers: '{s}'")
|
||||
return -1, -1
|
||||
self.log.debug(f"Invalid packet: Could not parse pid/vid from packet: '{data}'")
|
||||
return -1, -1
|
||||
|
||||
async def _spawn_car(self, data):
|
||||
car_data = data[2:]
|
||||
car_id = next((i for i, car in enumerate(self.cars) if car is None), len(self.cars))
|
||||
cars_count = len(self.cars) - self.cars.count(None)
|
||||
if self._snowman['id'] != -1:
|
||||
cars_count -= 1 # -1 for unicycle
|
||||
self.log.debug(f"car_id={car_id}, cars_count={cars_count}")
|
||||
car_json = {}
|
||||
try:
|
||||
car_json = json.loads(car_data[car_data.find("{"):])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Invalid car_json: Error: {e}; Data: {car_data}")
|
||||
allow = True
|
||||
allow_snowman = True
|
||||
over_spawn = False
|
||||
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
|
||||
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
# TODO: handle event onCarSpawn
|
||||
pass
|
||||
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
||||
snowman = car_json.get("jbm") == "unicycle"
|
||||
if allow and config.Game['max_cars'] > cars_count or (snowman and allow_snowman) or over_spawn:
|
||||
if snowman:
|
||||
unicycle_id = self._snowman['id']
|
||||
if unicycle_id != -1:
|
||||
self.log.debug(f"Delete old unicycle: unicycle_id={unicycle_id}")
|
||||
self._cars[unicycle_id] = None
|
||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||
self._snowman = {"id": car_id, "packet": pkt}
|
||||
self.log.debug(f"Unicycle spawn accepted: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Car spawn accepted: car_id={car_id}")
|
||||
self._cars[car_id] = {
|
||||
"packet": pkt,
|
||||
"json": car_json,
|
||||
"json_ok": bool(car_json),
|
||||
"snowman": snowman,
|
||||
"over_spawn": (snowman and allow_snowman) or over_spawn
|
||||
}
|
||||
await self._send(pkt, to_all=True, to_self=True)
|
||||
else:
|
||||
await self._send(pkt)
|
||||
des = f"Od:{self.cid}-{car_id}"
|
||||
await self._send(des)
|
||||
|
||||
async def _delete_car(self, raw_data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
|
||||
if car_id != -1 and self.cars[car_id]:
|
||||
|
||||
admin_allow = False # Delete from admin, for example...
|
||||
ev_data_list = ev.call_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
|
||||
d2 = await ev.call_async_event("onCarDelete", car=self.cars[car_id], car_id=car_id, player=self)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
# TODO: handle event onCarDelete
|
||||
pass
|
||||
|
||||
if cid == self.cid or admin_allow:
|
||||
await self._send(raw_data, to_all=True, to_self=True)
|
||||
car = self.cars[car_id]
|
||||
if car['snowman']:
|
||||
self.log.debug(f"Snowman found")
|
||||
unicycle_id = self._snowman['id']
|
||||
self._snowman['id'] = -1
|
||||
self._cars[unicycle_id] = None
|
||||
self._cars[car_id] = None
|
||||
await self._send(f"Od:{self.cid}-{car_id}", to_all=True, to_self=True)
|
||||
self.log.debug(f"Deleted car: car_id={car_id}")
|
||||
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _edit_car(self, raw_data, data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
if car_id != -1 and self.cars[car_id]:
|
||||
client = self.__Core.get_client(cid=cid)
|
||||
if client:
|
||||
car = client.cars[car_id]
|
||||
new_car_json = {}
|
||||
try:
|
||||
new_car_json = json.loads(data[data.find("{"):])
|
||||
except Exception as e:
|
||||
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {data}")
|
||||
|
||||
allow = False
|
||||
admin_allow = False
|
||||
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
|
||||
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
|
||||
ev_data_list.extend(d2)
|
||||
for ev_data in ev_data_list:
|
||||
# TODO: handle event onCarEdited
|
||||
pass
|
||||
|
||||
if cid == self.cid or allow or admin_allow:
|
||||
if car['snowman']:
|
||||
unicycle_id = self._snowman['id']
|
||||
self._snowman['id'] = -1
|
||||
self.log.debug(f"Delete snowman")
|
||||
await self._send(f"Od:{self.cid}-{unicycle_id}", to_all=True, to_self=True)
|
||||
self._cars[unicycle_id] = None
|
||||
else:
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
if car['json_ok']:
|
||||
old_car_json = car['json']
|
||||
old_car_json.update(new_car_json)
|
||||
car['json'] = old_car_json
|
||||
self.log.debug(f"Updated car: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _reset_car(self, raw_data):
|
||||
cid, car_id = self._get_cid_vid(raw_data)
|
||||
if car_id != -1 and cid == self.cid and self.cars[car_id]:
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
ev.call_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
|
||||
await ev.call_async_event("onCarReset", car=self.cars[car_id], car_id=car_id, player=self)
|
||||
self.log.debug(f"Car reset: car_id={car_id}")
|
||||
else:
|
||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||
|
||||
async def _handle_car_codes(self, raw_data):
|
||||
if len(raw_data) < 6:
|
||||
return
|
||||
sub_code = raw_data[1]
|
||||
data = raw_data[3:]
|
||||
match sub_code:
|
||||
case "s": # Spawn car
|
||||
self.log.debug("Trying to spawn car")
|
||||
if data[0] == "0":
|
||||
await self._spawn_car(data)
|
||||
|
||||
case "d": # Delete car
|
||||
self.log.debug("Trying to delete car")
|
||||
await self._delete_car(raw_data)
|
||||
|
||||
case "c": # Edit car
|
||||
self.log.debug("Trying to edit car")
|
||||
await self._edit_car(raw_data, data)
|
||||
|
||||
case "r": # Reset car
|
||||
self.log.debug("Trying to reset car")
|
||||
await self._reset_car(raw_data)
|
||||
|
||||
case "t": # Broken details
|
||||
self.log.debug(f"Something changed/broken: {raw_data}")
|
||||
await self._send(raw_data, to_all=True, to_self=False)
|
||||
|
||||
case "m": # Move focus cat
|
||||
self.log.debug(f"Move focus to: {raw_data}")
|
||||
await self._send(raw_data, to_all=True, to_self=True)
|
||||
|
||||
async def _connected_handler(self):
|
||||
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
|
||||
# Client connected
|
||||
ev.call_event("onPlayerJoin", player=self)
|
||||
await ev.call_async_event("onPlayerJoin", player=self)
|
||||
|
||||
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
|
||||
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
|
||||
self._ready = True
|
||||
|
||||
for client in self.__Core.clients:
|
||||
if not client:
|
||||
continue
|
||||
for car in client.cars:
|
||||
if not car:
|
||||
continue
|
||||
await self._send(car['packet'])
|
||||
|
||||
async def _chat_handler(self, data):
|
||||
sup = data.find(":", 2)
|
||||
if sup == -1:
|
||||
await self._send("C:Server: Invalid message.")
|
||||
msg = data[sup + 2:]
|
||||
if not msg:
|
||||
self.log.debug("Tried to send an empty event, ignoring")
|
||||
return
|
||||
to_ev = {"message": msg, "player": self}
|
||||
ev_data_list = ev.call_event("onChatReceive", **to_ev)
|
||||
d2 = await ev.call_async_event("onChatReceive", **to_ev)
|
||||
ev_data_list.extend(d2)
|
||||
need_send = True
|
||||
for ev_data in ev_data_list:
|
||||
try:
|
||||
message = ev_data["message"]
|
||||
to_all = ev_data.get("to_all")
|
||||
if to_all is None:
|
||||
to_all = True
|
||||
to_self = ev_data.get("to_self")
|
||||
if to_self is None:
|
||||
to_self = True
|
||||
to_client = ev_data.get("to_client")
|
||||
writer = None
|
||||
if to_client:
|
||||
# noinspection PyProtectedMember
|
||||
writer = to_client._writer
|
||||
self.log.info(f"{message}" if to_all else f"{self.nick}: {msg}")
|
||||
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
||||
need_send = False
|
||||
except KeyError | AttributeError:
|
||||
self.log.error(f"Returns invalid data: {ev_data}")
|
||||
if need_send:
|
||||
self.log.info(f"{self.nick}: {msg}")
|
||||
await self._send(data, to_all=True)
|
||||
|
||||
async def _handle_codes(self, data):
|
||||
if not data:
|
||||
self.__alive = False
|
||||
return
|
||||
|
||||
# Codes: V W X Y
|
||||
if 89 >= data[0] >= 86:
|
||||
await self._send(data, to_all=True, to_self=False)
|
||||
return
|
||||
|
||||
try:
|
||||
data = data.decode()
|
||||
except UnicodeDecodeError:
|
||||
self.log.error(f"UnicodeDecodeError: {data}")
|
||||
return
|
||||
|
||||
# Codes: p, Z in udp_server.py
|
||||
match data[0]: # At data[0] code
|
||||
case "H": # Map load, client ready
|
||||
await self._connected_handler()
|
||||
|
||||
case "C": # Chat handler
|
||||
await self._chat_handler(data)
|
||||
|
||||
case "O": # Cars handler
|
||||
await self._handle_car_codes(data)
|
||||
|
||||
case "E": # Client events handler
|
||||
# TODO: Handle events from client
|
||||
pass
|
||||
|
||||
case "N":
|
||||
await self._send(data, to_all=True, to_self=False)
|
||||
|
||||
async def _looper(self):
|
||||
self._connect_time = time.monotonic()
|
||||
await self._send(f"P{self.cid}") # Send clientID
|
||||
await self._sync_resources()
|
||||
tasks = self.__tasks
|
||||
recv = asyncio.create_task(self._recv())
|
||||
tasks.append(recv)
|
||||
while self.__alive:
|
||||
if len(self.__packets_queue) > 0:
|
||||
for index, packet in enumerate(self.__packets_queue):
|
||||
# self.log.debug(f"Packet: {packet}")
|
||||
del self.__packets_queue[index]
|
||||
task = self._loop.create_task(self._handle_codes(packet))
|
||||
tasks.append(task)
|
||||
else:
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def _remove_me(self):
|
||||
await asyncio.sleep(0.3)
|
||||
self.__alive = False
|
||||
if (self.cid > 0 or self.nick is not None) and \
|
||||
self.__Core.clients_by_nick.get(self.nick):
|
||||
for i, car in enumerate(self.cars):
|
||||
if not car:
|
||||
continue
|
||||
self.log.debug(f"Removing car: car_id={i}")
|
||||
await self._send(f"Od:{self.cid}-{i}", to_all=True, to_self=False)
|
||||
if self.ready:
|
||||
await self._send(f"J{self.nick} disconnected!", to_all=True, to_self=False) # I'm disconnected.
|
||||
self.log.debug(f"Removing client")
|
||||
# TODO: i18n
|
||||
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.")
|
||||
self.__Core.clients[self.cid] = None
|
||||
self.__Core.clients_by_id.pop(self.cid)
|
||||
self.__Core.clients_by_nick.pop(self.nick)
|
||||
else:
|
||||
self.log.debug(f"Removing client; Closing connection...")
|
||||
try:
|
||||
if not self.__writer.is_closing():
|
||||
self.__writer.close()
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error while closing writer: {e}")
|
||||
try:
|
||||
_, down_w = self._down_sock
|
||||
if down_w and not down_w.is_closing():
|
||||
down_w.close()
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error while closing download writer: {e}")
|
||||
74
src/core/Client.pyi
Normal file
74
src/core/Client.pyi
Normal file
@@ -0,0 +1,74 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter, DatagramTransport
|
||||
from logging import Logger
|
||||
from typing import Tuple, List, Dict, Optional, Union
|
||||
|
||||
from core import Core, utils
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
|
||||
self._connect_time: float = 0.0
|
||||
self.__tasks = []
|
||||
self.__reader = reader
|
||||
self.__writer = writer
|
||||
self.__packets_queue = []
|
||||
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
|
||||
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
|
||||
self._log = utils.get_logger("client(id: )")
|
||||
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self.__Core: Core = core
|
||||
self._cid: int = -1
|
||||
self._key: str = None
|
||||
self.nick: str = None
|
||||
self.roles: str = None
|
||||
self._guest = True
|
||||
self.__alive = True
|
||||
self._ready = False
|
||||
self._cars: List[Optional[Dict[str, int]]] = []
|
||||
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
||||
|
||||
@property
|
||||
def _writer(self) -> StreamWriter: ...
|
||||
@property
|
||||
def log(self) -> Logger: ...
|
||||
@property
|
||||
def addr(self) -> Tuple[str, int]: ...
|
||||
@property
|
||||
def cid(self) -> int: ...
|
||||
@property
|
||||
def key(self) -> str: ...
|
||||
@property
|
||||
def guest(self) -> bool: ...
|
||||
@property
|
||||
def ready(self) -> bool: ...
|
||||
@property
|
||||
def cars(self) -> List[dict | None]: ...
|
||||
def is_disconnected(self) -> bool: ...
|
||||
async def kick(self, reason: str) -> None: ...
|
||||
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
|
||||
async def send_event(self, event_name: str, event_data: str) -> None: ...
|
||||
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
|
||||
async def _sync_resources(self) -> None: ...
|
||||
async def _recv(self, one=False) -> bytes | None: ...
|
||||
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
|
||||
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
|
||||
async def _spawn_car(self, data: str) -> None: ...
|
||||
async def _delete_car(self, raw_data: str) -> None: ...
|
||||
async def _edit_car(self, raw_data: str, data: str) -> None: ...
|
||||
async def _reset_car(self, raw_data: str) -> None: ...
|
||||
async def _handle_car_codes(self, data: str) -> None: ...
|
||||
async def _connected_handler(self) -> None: ...
|
||||
async def _chat_handler(self, data: str) -> None: ...
|
||||
async def _handle_codes(self, data: bytes) -> None: ...
|
||||
async def _looper(self) -> None: ...
|
||||
def _update_logger(self) -> None: ...
|
||||
async def _remove_me(self) -> None: ...
|
||||
@@ -2,7 +2,7 @@
|
||||
# File core.__init__.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.3
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
|
||||
@@ -10,8 +10,8 @@
|
||||
__title__ = 'KuiToi-Server'
|
||||
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
|
||||
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
|
||||
__version__ = '0.2.2'
|
||||
__build__ = 1113 # Я это считаю лог файлами
|
||||
__version__ = '0.4.1'
|
||||
__build__ = 1486 # Я это считаю лог файлами
|
||||
__author__ = 'SantaSpeen'
|
||||
__author_email__ = 'admin@kuitoi.su'
|
||||
__license__ = "FPA"
|
||||
@@ -19,7 +19,6 @@ __copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import os
|
||||
import webbrowser
|
||||
|
||||
import prompt_toolkit.shortcuts as shortcuts
|
||||
@@ -27,7 +26,7 @@ import prompt_toolkit.shortcuts as shortcuts
|
||||
from .utils import get_logger
|
||||
from core.core import Core
|
||||
from main import parser
|
||||
from modules import ConfigProvider, EventsSystem, PluginsLoader
|
||||
from modules import ConfigProvider, EventsSystem
|
||||
from modules import Console
|
||||
from modules import MultiLanguage
|
||||
|
||||
@@ -47,17 +46,17 @@ if args.config:
|
||||
config_provider = ConfigProvider(config_path)
|
||||
config = config_provider.open_config()
|
||||
builtins.config = config
|
||||
if config.Server['debug'] is True:
|
||||
config.enc = config.Options['encoding']
|
||||
if config.Options['debug'] is True:
|
||||
utils.set_debug_status()
|
||||
log.info("Debug enabled!")
|
||||
log = get_logger("core.init")
|
||||
log.debug("Debug mode enabled!")
|
||||
log.debug(f"Server config: {config}")
|
||||
|
||||
# i18n init
|
||||
log.debug("Initializing i18n...")
|
||||
ml = MultiLanguage()
|
||||
ml.set_language(args.language or config.Server['language'])
|
||||
ml.set_language(args.language or config.Options['language'])
|
||||
ml.builtins_hook()
|
||||
|
||||
log.debug("Initializing EventsSystem...")
|
||||
@@ -107,16 +106,8 @@ console.builtins_hook()
|
||||
console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop)
|
||||
console.add_command("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit)
|
||||
|
||||
log.debug("Initializing PluginsLoader...")
|
||||
if not os.path.exists("plugins"):
|
||||
os.mkdir("plugins")
|
||||
pl = PluginsLoader("plugins")
|
||||
pl.load_plugins()
|
||||
|
||||
builtins.B = 1
|
||||
builtins.KB = B * 1024
|
||||
builtins.MB = KB * 1024
|
||||
builtins.GB = MB * 1024
|
||||
builtins.TB = GB * 1024
|
||||
|
||||
log.info(i18n.init_ok)
|
||||
|
||||
385
src/core/core.py
385
src/core/core.py
@@ -1,300 +1,26 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.py
|
||||
# Written by: SantaSpeen
|
||||
# Version: 0.2.2
|
||||
# Version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import zlib
|
||||
from threading import Thread
|
||||
|
||||
import aiohttp
|
||||
import uvicorn
|
||||
|
||||
from core import utils
|
||||
from core.Client import Client
|
||||
from core.tcp_server import TCPServer
|
||||
from core.udp_server import UDPServer
|
||||
from modules import PluginsLoader
|
||||
from modules.WebAPISystem import app as webapp
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader, writer, core):
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.down_rw = (None, None)
|
||||
self.log = utils.get_logger("client(None:0)")
|
||||
self.addr = writer.get_extra_info("sockname")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.cid = -1
|
||||
self.key = None
|
||||
self.nick = None
|
||||
self.roles = None
|
||||
self.guest = True
|
||||
self.alive = True
|
||||
self.ready = False
|
||||
|
||||
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"Client Disconnected")
|
||||
self.alive = False
|
||||
return True
|
||||
else:
|
||||
self.log.debug(f"Client Alive")
|
||||
self.alive = True
|
||||
return False
|
||||
|
||||
async def kick(self, reason):
|
||||
if not self.alive:
|
||||
self.log.debug(f"Kick({reason}) skipped;")
|
||||
return
|
||||
self.log.info(f"Kicked with reason: \"{reason}\"")
|
||||
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
|
||||
self.alive = False
|
||||
# await self.remove_me()
|
||||
|
||||
async def tcp_send(self, data, to_all=False, writer=None):
|
||||
|
||||
# TNetwork.cpp; Line: 383
|
||||
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
|
||||
# [][][][][][]...[]
|
||||
# ^------^^---...-^
|
||||
# size data
|
||||
|
||||
if writer is None:
|
||||
writer = self.writer
|
||||
|
||||
if to_all:
|
||||
for client in self.Core.clients:
|
||||
if not client:
|
||||
continue
|
||||
await client.tcp_send(data)
|
||||
return
|
||||
|
||||
# self.log.debug(f"tcp_send({data})")
|
||||
if len(data) == 10:
|
||||
data += b"."
|
||||
header = len(data).to_bytes(4, "little", signed=True)
|
||||
self.log.debug(f'len: {len(data)}; send: {header + data}')
|
||||
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) # header: 4 bytes
|
||||
|
||||
int_header = 0
|
||||
for i in range(len(header)):
|
||||
int_header += header[i]
|
||||
|
||||
if int_header <= 0:
|
||||
await asyncio.sleep(0.1)
|
||||
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):
|
||||
real_size = end - start
|
||||
writer = self.down_rw[1] if d_sock else self.writer
|
||||
who = 'dwn' if d_sock else 'srv'
|
||||
self.log.debug(f"[{who}] Real size: {real_size/MB}mb; {real_size == end}, {real_size*2 == end}")
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
f.seek(start)
|
||||
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.")
|
||||
# break
|
||||
return real_size
|
||||
|
||||
# chunk_size = 125 * MB
|
||||
# if chunk_size > real_size:
|
||||
# chunk_size = real_size
|
||||
# chunks = math.floor(real_size / chunk_size)
|
||||
# self.log.debug(f"[{who}] s:{start}, e:{end}, c:{chunks}, cz:{chunk_size/MB}mb, rs:{real_size/MB}mb")
|
||||
# dw = 0
|
||||
# for chunk in range(1, chunks + 1):
|
||||
# chunk_end = start + (chunk_size * chunk)
|
||||
# chunk_start = chunk_end - chunk_size
|
||||
# # if chunk_start != 0:
|
||||
# # chunk_start -= 1
|
||||
# real_size -= chunk_size
|
||||
# if chunk_size > real_size:
|
||||
# chunk_end = real_size
|
||||
# self.log.debug(f"[{who}] Chunk: {chunk}; Start: {chunk_start}; End: {chunk_end/MB};")
|
||||
# with open(filename, 'rb') as f:
|
||||
# f.seek(chunk_start)
|
||||
# data = f.read(chunk_end)
|
||||
# try:
|
||||
# writer.write(data)
|
||||
# await writer.drain()
|
||||
# except ConnectionError:
|
||||
# self.alive = False
|
||||
# self.log.debug(f"[{who}] Disconnected")
|
||||
# break
|
||||
# dw += len(data)
|
||||
# del data
|
||||
# self.log.debug(f"[{who}] File sent.")
|
||||
# return dw
|
||||
|
||||
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")
|
||||
self.log.debug(f"Sending File: {file}")
|
||||
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
|
||||
if size == -1:
|
||||
await self.tcp_send(b"CO")
|
||||
await self.kick(f"Not allowed mod: " + file)
|
||||
return
|
||||
await self.tcp_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
|
||||
self.log.info(f"Requested mode: {file!r}")
|
||||
self.log.debug(f"Mode size: {size/MB}")
|
||||
|
||||
msize = math.floor(size / 2)
|
||||
# uploads = [
|
||||
# asyncio.create_task(self._split_load(0, msize, False, file)), # SplitLoad_0
|
||||
# asyncio.create_task(self._split_load(msize, size, True, file)) # SplitLoad_1
|
||||
# ]
|
||||
# await asyncio.wait(uploads)
|
||||
uploads = [
|
||||
self._split_load(0, msize, False, file),
|
||||
self._split_load(msize, 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}")
|
||||
self.log.debug(f"SplitLoad_0: {sl0/MB}mb; "
|
||||
f"SplitLoad_1: {sl1/MB}MB; At all ({ok}): Sent: {sent/MB}mb; Lost: {lost/MB}mb")
|
||||
if not ok:
|
||||
self.alive = False
|
||||
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.tcp_send(b"-")
|
||||
else:
|
||||
await self.tcp_send(bytes(mod_list, "utf-8"))
|
||||
elif data == b"Done":
|
||||
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
|
||||
break
|
||||
return
|
||||
|
||||
async def looper(self):
|
||||
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8")) # Send clientID
|
||||
await self.sync_resources()
|
||||
while self.alive:
|
||||
data = await self.recv()
|
||||
if data == b"":
|
||||
if not self.alive:
|
||||
break
|
||||
else:
|
||||
await asyncio.sleep(.2)
|
||||
self.is_disconnected()
|
||||
continue
|
||||
code = data.decode()[0]
|
||||
self.log.debug(f"Received code: {code}, data: {data}")
|
||||
match code:
|
||||
case "H":
|
||||
# Client connected
|
||||
self.ready = True
|
||||
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
|
||||
case "C":
|
||||
# Chat
|
||||
await self.tcp_send(data, to_all=True)
|
||||
|
||||
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}")
|
||||
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...")
|
||||
if not self.writer.is_closing():
|
||||
self.writer.close()
|
||||
_, down_w = self.down_rw
|
||||
if down_w and not down_w.is_closing():
|
||||
down_w.close()
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class Core:
|
||||
|
||||
def __init__(self):
|
||||
@@ -315,10 +41,16 @@ class Core:
|
||||
self.web_pool = webapp.data_pool
|
||||
self.web_stop = None
|
||||
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.2.0"
|
||||
self.lock_upload = False
|
||||
|
||||
def get_client(self, cid=None, nick=None):
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.1.1" # 20.07.2023
|
||||
|
||||
ev.register_event("get_player", self.get_client)
|
||||
|
||||
def get_client(self, cid=None, nick=None, from_ev=None):
|
||||
if from_ev is not None:
|
||||
return self.get_client(*from_ev['args'], **from_ev['kwargs'])
|
||||
if cid is not None:
|
||||
return self.clients_by_id.get(cid)
|
||||
if nick:
|
||||
@@ -336,7 +68,7 @@ class Core:
|
||||
break
|
||||
await asyncio.sleep(random.randint(3, 9) * 0.01)
|
||||
if not self.clients[cid]:
|
||||
client.cid = cid
|
||||
client._cid = cid
|
||||
self.clients_by_nick.update({client.nick: client})
|
||||
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
|
||||
self.clients_by_id.update({client.cid: client})
|
||||
@@ -366,16 +98,20 @@ class Core:
|
||||
|
||||
async def check_alive(self):
|
||||
maxp = config.Game['players']
|
||||
while self.run:
|
||||
await asyncio.sleep(1)
|
||||
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
if not client.ready:
|
||||
client.is_disconnected()
|
||||
continue
|
||||
await client.tcp_send(bytes(ca, "utf-8"))
|
||||
try:
|
||||
while self.run:
|
||||
await asyncio.sleep(1)
|
||||
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
|
||||
for client in self.clients:
|
||||
if not client:
|
||||
continue
|
||||
if not client.ready:
|
||||
client.is_disconnected()
|
||||
continue
|
||||
await client._send(ca)
|
||||
except Exception as e:
|
||||
self.log.error("Error in check_alive.")
|
||||
self.log.exception(e)
|
||||
|
||||
@staticmethod
|
||||
def start_web():
|
||||
@@ -397,6 +133,7 @@ class Core:
|
||||
async def heartbeat(self, test=False):
|
||||
if config.Auth["private"] or self.direct:
|
||||
if test:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Server runnig in Direct connect mode.")
|
||||
self.direct = True
|
||||
return
|
||||
@@ -411,10 +148,10 @@ class Core:
|
||||
modstotal = len(self.mods_list) - 1
|
||||
while self.run:
|
||||
try:
|
||||
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
|
||||
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
|
||||
"private": config.Auth['private'], "version": self.BeamMP_version,
|
||||
"clientversion": self.client_major_version,
|
||||
data = {"uuid": config.Auth["key"], "players": len(self.clients_by_id),
|
||||
"maxplayers": config.Game["players"], "port": config.Server["server_port"],
|
||||
"map": f"/levels/{config.Game['map']}/info.json", "private": config.Auth['private'],
|
||||
"version": self.BeamMP_version, "clientversion": self.client_major_version,
|
||||
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
|
||||
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
|
||||
self.log.debug(f"Auth: data {data}")
|
||||
@@ -447,6 +184,7 @@ class Core:
|
||||
status = body.get("status")
|
||||
msg = body.get("msg")
|
||||
if status == "2000":
|
||||
# TODO: i18n
|
||||
self.log.info(f"Authenticated! {msg}")
|
||||
elif status == "200":
|
||||
self.log.info(f"Resumed authenticated session. {msg}")
|
||||
@@ -458,11 +196,11 @@ class Core:
|
||||
else:
|
||||
self.direct = True
|
||||
if test:
|
||||
self.log.error("Cannot auth...")
|
||||
if not config.Auth['private']:
|
||||
raise KeyboardInterrupt
|
||||
if test:
|
||||
# TODO: i18n
|
||||
self.log.error("Cannot authenticate server.")
|
||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
||||
# if not config.Auth['private']:
|
||||
# raise KeyboardInterrupt
|
||||
|
||||
if test:
|
||||
return ok
|
||||
@@ -472,13 +210,19 @@ class Core:
|
||||
self.log.error(f"Error in heartbeat: {e}")
|
||||
|
||||
async def main(self):
|
||||
self.run = True
|
||||
self.tcp = self.tcp(self, self.server_ip, self.server_port)
|
||||
self.udp = self.udp(self, self.server_ip, self.server_port)
|
||||
console.add_command(
|
||||
"list",
|
||||
lambda x: f"Players list: {self.get_clients_list(True)}"
|
||||
)
|
||||
|
||||
self.log.debug("Initializing PluginsLoader...")
|
||||
if not os.path.exists("plugins"):
|
||||
os.mkdir("plugins")
|
||||
pl = PluginsLoader("plugins")
|
||||
await pl.load()
|
||||
|
||||
try:
|
||||
# WebApi Start
|
||||
if config.WebAPI["enabled"]:
|
||||
@@ -502,40 +246,49 @@ class Core:
|
||||
self.mods_list.append({"path": path, "size": size})
|
||||
self.mods_list[0] += size
|
||||
self.log.debug(f"mods_list: {self.mods_list}")
|
||||
lmods = len(self.mods_list) - 1
|
||||
if lmods > 0:
|
||||
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
len_mods = len(self.mods_list) - 1
|
||||
if len_mods > 0:
|
||||
# TODO: i18n
|
||||
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
||||
self.log.info(i18n.init_ok)
|
||||
|
||||
await self.heartbeat(True)
|
||||
for i in range(int(config.Game["players"] * 1.3)):
|
||||
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
|
||||
self.clients.append(None)
|
||||
tasks = []
|
||||
# self.udp.start,
|
||||
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
for task in nrtasks:
|
||||
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
|
||||
for task in f_tasks:
|
||||
tasks.append(asyncio.create_task(task()))
|
||||
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
||||
|
||||
await ev.call_async_event("_plugins_start")
|
||||
|
||||
self.run = True
|
||||
self.log.info(i18n.start)
|
||||
ev.call_event("on_started")
|
||||
await t
|
||||
# Wait the end.
|
||||
ev.call_event("onServerStarted")
|
||||
await ev.call_async_event("onServerStarted")
|
||||
await t # Wait end.
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.error(f"Exception: {e}")
|
||||
self.log.exception(e)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.tcp.stop()
|
||||
# self.udp.stop()
|
||||
self.run = False
|
||||
self.tcp.stop()
|
||||
self.udp._stop()
|
||||
await self.stop()
|
||||
|
||||
def start(self):
|
||||
asyncio.run(self.main())
|
||||
|
||||
def stop(self):
|
||||
async def stop(self):
|
||||
ev.call_event("onServerStopped")
|
||||
await ev.call_async_event("onServerStopped")
|
||||
await ev.call_async_event("_plugins_unload")
|
||||
self.run = False
|
||||
self.log.info(i18n.stop)
|
||||
if config.WebAPI["enabled"]:
|
||||
asyncio.run(self.web_stop())
|
||||
exit(0)
|
||||
# exit(0)
|
||||
|
||||
@@ -1,46 +1,19 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.core.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Version 0.2.2
|
||||
# Version 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import StreamWriter, StreamReader
|
||||
from threading import Thread
|
||||
from typing import Callable, List, Dict, Tuple
|
||||
from typing import Callable, List, Dict
|
||||
|
||||
from core import utils
|
||||
from .Client import Client
|
||||
from .tcp_server import TCPServer
|
||||
from .udp_server import UDPServer
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
|
||||
self.log = utils.get_logger("client(id: )")
|
||||
self.addr = writer.get_extra_info("sockname")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.cid: int = -1
|
||||
self.key: str = None
|
||||
self.nick: str = None
|
||||
self.roles: str = None
|
||||
self.guest = True
|
||||
self.alive = True
|
||||
self.ready = False
|
||||
def is_disconnected(self) -> bool: ...
|
||||
async def kick(self, reason: str) -> None: ...
|
||||
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
|
||||
async def sync_resources(self) -> None: ...
|
||||
async def recv(self) -> bytes: ...
|
||||
async def _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: ...
|
||||
|
||||
class Core:
|
||||
def __init__(self):
|
||||
self.log = utils.get_logger("core")
|
||||
@@ -59,6 +32,7 @@ class Core:
|
||||
self.udp = UDPServer
|
||||
self.web_thread: Thread = None
|
||||
self.web_stop: Callable = lambda: None
|
||||
self.lock_upload = False
|
||||
self.client_major_version = "2.0"
|
||||
self.BeamMP_version = "3.2.0"
|
||||
def get_client(self, cid=None, nick=None) -> Client | None: ...
|
||||
@@ -72,4 +46,4 @@ class Core:
|
||||
async def heartbeat(self, test=False) -> None: ...
|
||||
async def main(self) -> None: ...
|
||||
def start(self) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
async def stop(self) -> None: ...
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -12,6 +12,7 @@ import aiohttp
|
||||
from core import utils
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
class TCPServer:
|
||||
def __init__(self, core, host, port):
|
||||
self.log = utils.get_logger("TCPServer")
|
||||
@@ -23,37 +24,42 @@ class TCPServer:
|
||||
|
||||
async def auth_client(self, reader, writer):
|
||||
client = self.Core.create_client(reader, writer)
|
||||
# TODO: i18n
|
||||
self.log.info(f"Identifying new ClientConnection...")
|
||||
data = await client.recv()
|
||||
data = await client._recv(True)
|
||||
self.log.debug(f"Version: {data}")
|
||||
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
|
||||
# TODO: i18n
|
||||
await client.kick("Outdated Version.")
|
||||
return False, client
|
||||
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(True)
|
||||
self.log.debug(f"Key: {data}")
|
||||
if len(data) > 50:
|
||||
# TODO: i18n
|
||||
await client.kick("Invalid Key (too long)!")
|
||||
return False, client
|
||||
client.key = data.decode("utf-8")
|
||||
ev.call_event("auth_sent_key", client)
|
||||
client._key = data.decode("utf-8")
|
||||
ev.call_event("onPlayerSentKey", player=client)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
url = 'https://auth.beammp.com/pkToUser'
|
||||
async with session.post(url, data={'key': client.key}) as response:
|
||||
async with session.post(url, data={'key': client._key}) as response:
|
||||
res = await response.json()
|
||||
self.log.debug(f"res: {res}")
|
||||
if res.get("error"):
|
||||
# TODO: i18n
|
||||
await client.kick('Invalid key! Please restart your game.')
|
||||
return False, client
|
||||
client.nick = res["username"]
|
||||
client.roles = res["roles"]
|
||||
client.guest = res["guest"]
|
||||
client._guest = res["guest"]
|
||||
# noinspection PyProtectedMember
|
||||
client._update_logger()
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Auth error: {e}")
|
||||
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
|
||||
return False, client
|
||||
@@ -62,15 +68,18 @@ class TCPServer:
|
||||
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("auth_ok", client)
|
||||
ev.call_event("onPlayerAuthenticated", player=client)
|
||||
|
||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||
# TODO: i18n
|
||||
await client.kick("Server full!")
|
||||
return False, client
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.info("Identification success")
|
||||
await self.Core.insert_client(client)
|
||||
|
||||
@@ -81,7 +90,7 @@ class TCPServer:
|
||||
cid = (await reader.read(1))[0]
|
||||
client = self.Core.get_client(cid=cid)
|
||||
if client:
|
||||
client.down_rw = (reader, writer)
|
||||
client._down_sock = (reader, writer)
|
||||
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
|
||||
else:
|
||||
writer.close()
|
||||
@@ -94,7 +103,7 @@ class TCPServer:
|
||||
case "C":
|
||||
result, client = await self.auth_client(reader, writer)
|
||||
if result:
|
||||
await client.looper()
|
||||
await client._looper()
|
||||
return result, client
|
||||
case "D":
|
||||
await self.set_down_rw(reader, writer)
|
||||
@@ -103,6 +112,7 @@ class TCPServer:
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
case _:
|
||||
# TODO: i18n
|
||||
self.log.error(f"Unknown code: {code}")
|
||||
writer.close()
|
||||
return False, None
|
||||
@@ -119,11 +129,12 @@ class TCPServer:
|
||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
||||
_, cl = await self.handle_code(code, reader, writer)
|
||||
if cl:
|
||||
await cl.remove_me()
|
||||
await cl._remove_me()
|
||||
del cl
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.error("Error while connecting..")
|
||||
# TODO: i18n
|
||||
self.log.error("Error while handling connection...")
|
||||
self.log.exception(e)
|
||||
traceback.print_exc()
|
||||
break
|
||||
@@ -133,15 +144,18 @@ class TCPServer:
|
||||
self.run = True
|
||||
try:
|
||||
server = await asyncio.start_server(self.handle_client, self.host, self.port,
|
||||
backlog=int(config.Game["players"] * 1.3))
|
||||
backlog=int(config.Game["players"] * 2.3))
|
||||
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
|
||||
while True:
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
except OSError as e:
|
||||
# TODO: i18n
|
||||
self.log.error("Cannot bind port")
|
||||
raise e
|
||||
except BaseException as e:
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
raise e
|
||||
finally:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.tcp_server.pyi
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
@@ -9,7 +9,7 @@ from asyncio import StreamWriter, StreamReader
|
||||
from typing import Tuple
|
||||
|
||||
from core import utils, Core
|
||||
from core.core import Client
|
||||
from core.Client import Client
|
||||
|
||||
|
||||
class TCPServer:
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from core import utils
|
||||
|
||||
|
||||
class UDPServer:
|
||||
# noinspection PyProtectedMember
|
||||
class UDPServer(asyncio.DatagramTransport):
|
||||
transport = None
|
||||
|
||||
def __init__(self, core, host, port):
|
||||
def __init__(self, core, host=None, port=None):
|
||||
super().__init__()
|
||||
self.log = utils.get_logger("UDPServer")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
@@ -20,41 +22,77 @@ class UDPServer:
|
||||
self.port = port
|
||||
self.run = False
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
while True:
|
||||
try:
|
||||
data = await reader.read(1)
|
||||
if not data:
|
||||
break
|
||||
code = data.decode()
|
||||
self.log.debug(f"Received {code!r} from {writer.get_extra_info('sockname')!r}")
|
||||
# await self.handle_code(code, reader, writer)
|
||||
# task = asyncio.create_task(self.handle_code(code, reader, writer))
|
||||
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
|
||||
if not writer.is_closing():
|
||||
writer.close()
|
||||
self.log.debug("Disconnected.")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.error("Error while connecting..")
|
||||
self.log.error(f"Error: {e}")
|
||||
traceback.print_exc()
|
||||
break
|
||||
def connection_made(self, transport): ...
|
||||
|
||||
async def start(self):
|
||||
self.log.debug("Starting UDP server.")
|
||||
self.run = True
|
||||
async def handle_datagram(self, data, addr):
|
||||
try:
|
||||
pass
|
||||
cid = data[0] - 1
|
||||
code = data[2:3].decode()
|
||||
|
||||
client = self.Core.get_client(cid=cid)
|
||||
if client:
|
||||
match code:
|
||||
case "p": # Ping packet
|
||||
ev.call_event("onSentPing")
|
||||
self.transport.sendto(b"p", addr)
|
||||
case "Z": # Position packet
|
||||
if client._udp_sock != (self.transport, addr):
|
||||
client._udp_sock = (self.transport, addr)
|
||||
self.log.debug(f"Set UDP Sock for CID: {cid}")
|
||||
ev.call_event("onChangePosition")
|
||||
if client:
|
||||
await client._send(data[2:], to_all=True, to_self=False, to_udp=True)
|
||||
case _:
|
||||
self.log.debug(f"[{cid}] Unknown code: {code}")
|
||||
else:
|
||||
self.log.debug(f"Client not found.")
|
||||
|
||||
except Exception as e:
|
||||
self.log.error(f"Error handle_datagram: {e}")
|
||||
|
||||
def datagram_received(self, *args, **kwargs):
|
||||
self.loop.create_task(self.handle_datagram(*args, **kwargs))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if exc is not None and exc != KeyboardInterrupt:
|
||||
self.log.debug(f'Connection raised: {exc}')
|
||||
self.log.debug(f'Disconnected.')
|
||||
|
||||
def error_received(self, exc):
|
||||
self.log.debug(f'error_received: {exc}')
|
||||
self.log.exception(exc)
|
||||
self.connection_lost(exc)
|
||||
self.transport.close()
|
||||
|
||||
async def _start(self):
|
||||
self.log.debug("Starting UDP server.")
|
||||
try:
|
||||
while self.Core.run:
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
d = UDPServer
|
||||
self.transport, p = await self.loop.create_datagram_endpoint(
|
||||
lambda: d(self.Core),
|
||||
local_addr=(self.host, self.port)
|
||||
)
|
||||
d.transport = self.transport
|
||||
|
||||
if not self.run:
|
||||
self.log.debug(f"UDP server started on {self.transport.get_extra_info('sockname')}")
|
||||
|
||||
self.run = True
|
||||
while not self.transport.is_closing():
|
||||
await asyncio.sleep(0.2)
|
||||
except OSError as e:
|
||||
self.log.error("Cannot bind port or other error")
|
||||
raise e
|
||||
except BaseException as e:
|
||||
self.log.exception(e)
|
||||
except Exception as e:
|
||||
self.log.error(f"Error: {e}")
|
||||
raise e
|
||||
self.log.exception(e)
|
||||
finally:
|
||||
self.run = False
|
||||
self.Core.run = False
|
||||
|
||||
def stop(self):
|
||||
def _stop(self):
|
||||
self.log.debug("Stopping UDP server")
|
||||
self.transport.close()
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File core.udp_server.py
|
||||
# Written by: SantaSpeen
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
from asyncio import DatagramTransport
|
||||
from typing import Tuple, List
|
||||
|
||||
from core import utils
|
||||
from core.core import Core
|
||||
|
||||
|
||||
class UDPServer:
|
||||
class UDPServer(asyncio.DatagramTransport):
|
||||
transport: DatagramTransport = None
|
||||
|
||||
def __init__(self, core, host, port):
|
||||
def __init__(self, core: Core, host=None, port=None, transport=None):
|
||||
self.log = utils.get_logger("UDPServer")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.Core = core
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.run = False
|
||||
async def handle_client(self, srv_sock) -> None: ...
|
||||
async def start(self) -> None: ...
|
||||
|
||||
async def stop(self) -> None: ...
|
||||
# self.transport: DatagramTransport = None
|
||||
def connection_made(self, transport: DatagramTransport): ...
|
||||
async def handle_datagram(self, data: bytes, addr: Tuple[str, int]):
|
||||
def datagram_received(self, data: bytes, addr: Tuple[str, int]): ...
|
||||
async def _start(self) -> None: ...
|
||||
async def _stop(self) -> None: ...
|
||||
@@ -2,7 +2,7 @@
|
||||
# File core.utils.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Core version: 0.2.2
|
||||
# Core version: 0.4.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import datetime
|
||||
@@ -20,8 +20,8 @@ logging.basicConfig(level=log_level, format=log_format)
|
||||
if not os.path.exists(log_dir):
|
||||
os.mkdir(log_dir)
|
||||
if os.path.exists(log_file):
|
||||
mtime = os.path.getmtime(log_file)
|
||||
gz_path = log_dir + datetime.datetime.fromtimestamp(mtime).strftime('%d.%m.%Y') + "-%s.tar.gz"
|
||||
ftime = os.path.getmtime(log_file)
|
||||
gz_path = log_dir + datetime.datetime.fromtimestamp(ftime).strftime('%d.%m.%Y') + "-%s.tar.gz"
|
||||
index = 1
|
||||
while True:
|
||||
if not os.path.exists(gz_path % index):
|
||||
@@ -33,11 +33,15 @@ if os.path.exists(log_file):
|
||||
if os.path.exists(file):
|
||||
tar.add(file, os.path.basename(file))
|
||||
os.remove(file)
|
||||
fh = logging.FileHandler(log_file, encoding='utf-8')
|
||||
fh = logging.FileHandler(log_file, encoding="utf-8")
|
||||
fh.setFormatter(logging.Formatter(log_format))
|
||||
|
||||
|
||||
def get_logger(name):
|
||||
try:
|
||||
fh.encoding = config.enc
|
||||
except NameError:
|
||||
fh.encoding = "utf-8"
|
||||
log = logging.getLogger(name=name)
|
||||
log.addHandler(fh)
|
||||
log.level = log_level
|
||||
|
||||
@@ -14,17 +14,13 @@ parser.add_argument('-v', '--version', action="store_true", help='Print version
|
||||
parser.add_argument('--config', help='Patch to config file.', nargs='?', default=None, type=str)
|
||||
parser.add_argument('--language', help='Setting localisation.', nargs='?', default=None, type=str)
|
||||
|
||||
run = True
|
||||
|
||||
|
||||
def main():
|
||||
from core import Core
|
||||
core = Core()
|
||||
try:
|
||||
core.start()
|
||||
Core().start()
|
||||
except KeyboardInterrupt:
|
||||
core.run = False
|
||||
core.stop()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import secrets
|
||||
|
||||
|
||||
class Config:
|
||||
Auth: dict
|
||||
Game: dict
|
||||
Server: dict
|
||||
WebAPI: dict
|
||||
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
|
||||
self.Auth = auth or {"key": None, "private": True}
|
||||
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
|
||||
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
|
||||
"server_ip": "0.0.0.0", "server_port": 30814}
|
||||
self.Options = options or {"language": "en", "encoding": "utf8", "speed_limit": 0, "use_queue": False,
|
||||
"debug": False}
|
||||
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
|
||||
"secret_key": secrets.token_hex(16)}
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
|
||||
class config (Config): ...
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.config_provider.config_provider.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.1
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import os
|
||||
@@ -11,13 +10,14 @@ import secrets
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, auth=None, game=None, server=None, web=None):
|
||||
def __init__(self, auth=None, game=None, server=None, options=None, web=None):
|
||||
self.Auth = auth or {"key": None, "private": True}
|
||||
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
|
||||
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "language": "en",
|
||||
"server_ip": "0.0.0.0", "server_port": 30814, "debug": False}
|
||||
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!",
|
||||
"server_ip": "0.0.0.0", "server_port": 30814}
|
||||
self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False,
|
||||
"debug": False}
|
||||
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
|
||||
"secret_key": secrets.token_hex(16)}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File core.config_provider.py
|
||||
# File modules.ConsoleSystem.console_system.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.1
|
||||
# Version 1.2
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
@@ -87,7 +87,7 @@ class Console:
|
||||
print()
|
||||
raw = True
|
||||
|
||||
message = str()
|
||||
message = "\n"
|
||||
max_len = self.__get_max_len(self.__func.keys())
|
||||
if max_len < 7:
|
||||
max_len = 7
|
||||
|
||||
@@ -1,46 +1,108 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.EventsSystem.events_system.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import builtins
|
||||
import inspect
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
class EventsSystem:
|
||||
|
||||
def __init__(self):
|
||||
self.__events = {
|
||||
"on_started": [self.on_started],
|
||||
"on_stop": [self.on_stop],
|
||||
"on_auth": [self.on_auth]
|
||||
}
|
||||
# TODO: default events
|
||||
self.log = get_logger("EventsSystem")
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.as_tasks = []
|
||||
self.__events = {
|
||||
"onServerStarted": [],
|
||||
"onPlayerSentKey": [], # Only sync
|
||||
"onPlayerAuthenticated": [], # Only sync
|
||||
"onPlayerJoin": [],
|
||||
"onChatReceive": [],
|
||||
"onCarSpawn": [],
|
||||
"onCarDelete": [],
|
||||
"onCarEdited": [],
|
||||
"onCarReset": [],
|
||||
"onSentPing": [], # Only sync
|
||||
"onChangePosition": [], # Only sync
|
||||
"onServerStopped": [],
|
||||
}
|
||||
self.__async_events = {
|
||||
"onServerStarted": [],
|
||||
"onPlayerJoin": [],
|
||||
"onChatReceive": [],
|
||||
"onCarSpawn": [],
|
||||
"onCarDelete": [],
|
||||
"onCarEdited": [],
|
||||
"onCarReset": [],
|
||||
"onServerStopped": []
|
||||
}
|
||||
|
||||
def builtins_hook(self):
|
||||
self.log.debug("used builtins_hook")
|
||||
builtins.ev = self
|
||||
|
||||
def register_event(self, event_name, event_func):
|
||||
def register_event(self, event_name, event_func, async_event=False):
|
||||
self.log.debug(f"register_event({event_name}, {event_func}):")
|
||||
if not callable(event_func):
|
||||
# TODO: i18n
|
||||
self.log.error(f"Cannot add event '{event_name}'. "
|
||||
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
|
||||
return
|
||||
if event_name not in self.__events:
|
||||
self.__events.update({str(event_name): [event_func]})
|
||||
if async_event or inspect.iscoroutinefunction(event_func):
|
||||
if event_name not in self.__async_events:
|
||||
self.__async_events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__async_events[event_name].append(event_func)
|
||||
else:
|
||||
self.__events[event_name].append(event_func)
|
||||
if event_name not in self.__events:
|
||||
self.__events.update({str(event_name): [event_func]})
|
||||
else:
|
||||
self.__events[event_name].append(event_func)
|
||||
|
||||
async def call_async_event(self, event_name, *args, **kwargs):
|
||||
self.log.debug(f"Calling async event: '{event_name}'")
|
||||
funcs_data = []
|
||||
if event_name in self.__async_events.keys():
|
||||
for func in self.__async_events[event_name]:
|
||||
try:
|
||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||
data = await func(event_data)
|
||||
funcs_data.append(data)
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
|
||||
self.log.exception(e)
|
||||
else:
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_event()?. Just skipping it...")
|
||||
|
||||
return funcs_data
|
||||
|
||||
def call_event(self, event_name, *args, **kwargs):
|
||||
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
|
||||
self.log.debug(f"Calling sync event: '{event_name}'")
|
||||
funcs_data = []
|
||||
|
||||
def call_event(self, event_name, *data):
|
||||
self.log.debug(f"Using event '{event_name}'")
|
||||
if event_name in self.__events.keys():
|
||||
for event in self.__events[event_name]:
|
||||
event(*data)
|
||||
for func in self.__events[event_name]:
|
||||
try:
|
||||
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
|
||||
funcs_data.append(func(event_data))
|
||||
except Exception as e:
|
||||
# TODO: i18n
|
||||
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
|
||||
self.log.exception(e)
|
||||
else:
|
||||
self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
|
||||
# TODO: i18n
|
||||
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
|
||||
|
||||
def on_started(self):
|
||||
pass
|
||||
|
||||
def on_stop(self):
|
||||
pass
|
||||
|
||||
def on_auth(self, client):
|
||||
pass
|
||||
return funcs_data
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class EventsSystem:
|
||||
@staticmethod
|
||||
def register_event(self, event_name, event_func): ...
|
||||
def register_event(event_name, event_func): ...
|
||||
@staticmethod
|
||||
def call_event(self, event_name, *data): ...
|
||||
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
|
||||
@staticmethod
|
||||
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
|
||||
class ev(EventsSystem): ...
|
||||
|
||||
@@ -1,52 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.PluginsLoader.plugins_loader.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
from threading import Thread
|
||||
|
||||
from core import get_logger
|
||||
|
||||
|
||||
# TODO: call_client_event, get_player, get_players, GetPlayerCount
|
||||
class KuiToi:
|
||||
_plugins_dir = ""
|
||||
|
||||
def __init__(self, name=None):
|
||||
if name is None:
|
||||
raise Exception("BeamMP: Name is required")
|
||||
self.log = get_logger(f"PluginsLoader | {name}")
|
||||
self.name = name
|
||||
raise AttributeError("KuiToi: Name is required")
|
||||
self.log = get_logger(f"Plugin | {name}")
|
||||
self.__name = name
|
||||
self.__dir = os.path.join(self._plugins_dir, self.__name)
|
||||
if not os.path.exists(self.__dir):
|
||||
os.mkdir(self.__dir)
|
||||
|
||||
def set_name(self, name):
|
||||
self.name = name
|
||||
@property
|
||||
def name(self):
|
||||
return self.__name
|
||||
|
||||
@staticmethod
|
||||
def register_event(event_name, event_func):
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
# You chell not pass
|
||||
pass
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return self.__dir
|
||||
|
||||
@dir.setter
|
||||
def dir(self, value):
|
||||
# You chell not pass
|
||||
pass
|
||||
|
||||
@contextmanager
|
||||
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
|
||||
path = os.path.join(self.__dir, file)
|
||||
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
|
||||
# if not os.path.exists(path):
|
||||
# with open(path, 'x'): ...
|
||||
f = None
|
||||
try:
|
||||
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
|
||||
yield f
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
|
||||
def register_event(self, event_name, event_func):
|
||||
self.log.debug(f"Registering event {event_name}")
|
||||
ev.register_event(event_name, event_func)
|
||||
|
||||
@staticmethod
|
||||
def call_event(event_name, *data):
|
||||
ev.call_event(event_name, *data)
|
||||
def call_event(self, event_name, *data, **kwargs):
|
||||
self.log.debug(f"Called event {event_name}")
|
||||
ev.call_event(event_name, *data, **kwargs)
|
||||
|
||||
|
||||
class PluginsLoader:
|
||||
|
||||
def __init__(self, plugins_dir):
|
||||
self.__plugins = {}
|
||||
self.__plugins_dir = plugins_dir
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.plugins = {}
|
||||
self.plugins_tasks = []
|
||||
self.plugins_dir = plugins_dir
|
||||
self.log = get_logger("PluginsLoader")
|
||||
self.loaded_str = "Plugins: "
|
||||
ev.register_event("_plugins_start", self.start)
|
||||
ev.register_event("_plugins_unload", self.unload)
|
||||
console.add_command("plugins", lambda x: self.loaded_str[:-2])
|
||||
console.add_command("pl", lambda x: self.loaded_str[:-2])
|
||||
|
||||
def load_plugins(self):
|
||||
async def load(self):
|
||||
self.log.debug("Loading plugins...")
|
||||
files = os.listdir(self.__plugins_dir)
|
||||
files = os.listdir(self.plugins_dir)
|
||||
for file in files:
|
||||
if file.endswith(".py"):
|
||||
try:
|
||||
self.log.debug(f"Loading plugin: {file}")
|
||||
plugin = types.ModuleType('plugin')
|
||||
self.log.debug(f"Loading plugin: {file[:-3]}")
|
||||
plugin = types.ModuleType(file[:-3])
|
||||
plugin.KuiToi = KuiToi
|
||||
plugin.KuiToi._plugins_dir = self.plugins_dir
|
||||
plugin.print = print
|
||||
file = os.path.join(self.__plugins_dir, file)
|
||||
with open(f'{file}', 'r') as f:
|
||||
code = f.read().replace("import KuiToi\n", "")
|
||||
file_path = os.path.join(self.plugins_dir, file)
|
||||
plugin.__file__ = file_path
|
||||
with open(f'{file_path}', 'r', encoding=config.enc) as f:
|
||||
code = f.read()
|
||||
exec(code, plugin.__dict__)
|
||||
plugin.load()
|
||||
self.__plugins.update({file[:-3]: plugin})
|
||||
self.log.debug(f"Plugin loaded: {file}")
|
||||
|
||||
ok = True
|
||||
try:
|
||||
isfunc = inspect.isfunction
|
||||
if not isfunc(plugin.load):
|
||||
self.log.error('Function "def load():" not found.')
|
||||
ok = False
|
||||
if not isfunc(plugin.start):
|
||||
self.log.error('Function "def start():" not found.')
|
||||
ok = False
|
||||
if not isfunc(plugin.unload):
|
||||
self.log.error('Function "def unload():" not found.')
|
||||
ok = False
|
||||
if type(plugin.kt) != KuiToi:
|
||||
self.log.error(f'Attribute "kt" isn\'t KuiToi class. Plugin file: "{file_path}"')
|
||||
ok = False
|
||||
except AttributeError:
|
||||
ok = False
|
||||
if not ok:
|
||||
self.log.error(f'Plugin file: "{file_path}" is not a valid KuiToi plugin.')
|
||||
return
|
||||
|
||||
pl_name = plugin.kt.name
|
||||
if self.plugins.get(pl_name) is not None:
|
||||
raise NameError(f'Having plugins with identical names is not allowed; '
|
||||
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
|
||||
|
||||
plugin.open = plugin.kt.open
|
||||
iscorfunc = inspect.iscoroutinefunction
|
||||
self.plugins.update(
|
||||
{
|
||||
pl_name: {
|
||||
"plugin": plugin,
|
||||
"load": {
|
||||
"func": plugin.load,
|
||||
"async": iscorfunc(plugin.load)
|
||||
},
|
||||
"start": {
|
||||
"func": plugin.start,
|
||||
"async": iscorfunc(plugin.start)
|
||||
},
|
||||
"unload": {
|
||||
"func": plugin.unload,
|
||||
"async": iscorfunc(plugin.unload)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if self.plugins[pl_name]["load"]['async']:
|
||||
plugin.log.debug(f"I'm async")
|
||||
await plugin.load()
|
||||
else:
|
||||
plugin.log.debug(f"I'm sync")
|
||||
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
|
||||
th.start()
|
||||
th.join()
|
||||
self.loaded_str += f"{pl_name}:ok, "
|
||||
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error loading plugin: {file}; Error: {e}")
|
||||
# TODO: i18n
|
||||
self.loaded_str += f"{file}:no, "
|
||||
self.log.error(f"Error while loading plugin: {file}; Error: {e}")
|
||||
self.log.exception(e)
|
||||
|
||||
async def start(self, _):
|
||||
for pl_name, pl_data in self.plugins.items():
|
||||
try:
|
||||
if pl_data['start']['async']:
|
||||
self.log.debug(f"Start async plugin: {pl_name}")
|
||||
t = self.loop.create_task(pl_data['start']['func']())
|
||||
self.plugins_tasks.append(t)
|
||||
else:
|
||||
self.log.debug(f"Start sync plugin: {pl_name}")
|
||||
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
|
||||
th.start()
|
||||
self.plugins_tasks.append(th)
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
|
||||
async def unload(self, _):
|
||||
for pl_name, pl_data in self.plugins.items():
|
||||
try:
|
||||
if pl_data['unload']['async']:
|
||||
self.log.debug(f"Unload async plugin: {pl_name}")
|
||||
await pl_data['unload']['func']()
|
||||
else:
|
||||
self.log.debug(f"Unload sync plugin: {pl_name}")
|
||||
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
|
||||
th.start()
|
||||
th.join()
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
|
||||
@@ -122,6 +122,5 @@ def hack_fastapi():
|
||||
})
|
||||
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
|
||||
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
|
||||
print(LOGGING_CONFIG)
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
||||
"auth_empty_key": "The BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
@@ -17,32 +17,32 @@
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
|
||||
"GUI_enter_key_message": "Please enter the key:",
|
||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
|
||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Displays help page for COMMAND.",
|
||||
"man_for": "Help page for",
|
||||
"man_message_not_found": "man: Help page not found.",
|
||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
|
||||
"help_message_help": "Displays the names and short descriptions of commands.",
|
||||
"help_command": "Command",
|
||||
"help_message": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"help_message": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
# Developed by KuiToi Dev
|
||||
# File modules.i18n.i18n.py
|
||||
# Written by: SantaSpeen
|
||||
# Version 1.0
|
||||
# Version 1.3
|
||||
# Licence: FPA
|
||||
# (c) kuitoi.su 2023
|
||||
import builtins
|
||||
@@ -68,7 +68,9 @@ class i18n:
|
||||
|
||||
class MultiLanguage:
|
||||
|
||||
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
|
||||
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding=None):
|
||||
if encoding is None:
|
||||
encoding = config.enc
|
||||
if language is None:
|
||||
language = "en"
|
||||
self.__data = {}
|
||||
@@ -89,53 +91,53 @@ class MultiLanguage:
|
||||
else:
|
||||
# noinspection PyDictDuplicateKeys
|
||||
self.__data = {
|
||||
"": "Basic phases",
|
||||
"hello": "Hello from KuiToi-Server!",
|
||||
"config_path": "Use {} for config.",
|
||||
"init_ok": "Initializing ready.",
|
||||
"start": "Server started!",
|
||||
"stop": "Goodbye!",
|
||||
"": "Basic phases",
|
||||
"hello": "Greetings from KuiToi Server!",
|
||||
"config_path": "Use {} to configure.",
|
||||
"init_ok": "Initialization complete.",
|
||||
"start": "Server started!",
|
||||
"stop": "Server stopped!",
|
||||
|
||||
"": "Server auth",
|
||||
"auth_need_key": "BEAM key needed for starting the server!",
|
||||
"auth_empty_key": "Key is empty!",
|
||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
"": "Server auth",
|
||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
||||
"auth_empty_key": "The BeamMP key is empty!",
|
||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||
"auth_use_link": "Use this link: {}",
|
||||
|
||||
"": "GUI phases",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
||||
"GUI_enter_key_message": "Please type your key:",
|
||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
||||
"": "GUI phases",
|
||||
"GUI_yes": "Yes",
|
||||
"GUI_no": "No",
|
||||
"GUI_ok": "Ok",
|
||||
"GUI_cancel": "Cancel",
|
||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
|
||||
"GUI_enter_key_message": "Please enter the key:",
|
||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
|
||||
"": "Web phases",
|
||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
||||
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Display the manual page for COMMAND.",
|
||||
"man_for": "Manual for command",
|
||||
"man_message_not_found": "man: Manual message not found.",
|
||||
"man_command_not_found": "man: command \"{}\" not found!",
|
||||
"": "Command: man",
|
||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
||||
"help_message_man": "Displays help page for COMMAND.",
|
||||
"man_for": "Help page for",
|
||||
"man_message_not_found": "man: Help page not found.",
|
||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
||||
"help_message_help": "Display names and brief descriptions of available commands",
|
||||
"help_command": "Command",
|
||||
"help_message": "Help message",
|
||||
"help_message_not_found": "No help message found",
|
||||
"": "Command: help",
|
||||
"man_message_help": "help - Displays the names and short descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands and a brief description of each command.",
|
||||
"help_message_help": "Displays the names and short descriptions of commands.",
|
||||
"help_command": "Command",
|
||||
"help_message": "Description",
|
||||
"help_message_not_found": "No description available.",
|
||||
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_stop": "Server shutdown.",
|
||||
"": "Command: stop",
|
||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||
"help_message_stop": "Stops the server.",
|
||||
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
||||
"help_message_exit": "Server shutdown."
|
||||
}
|
||||
"": "Command: exit",
|
||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
||||
"help_message_exit": "Stops the server."
|
||||
}
|
||||
self.__i18n = i18n(self.__data)
|
||||
|
||||
def open_file(self):
|
||||
|
||||
Reference in New Issue
Block a user