45 Commits

Author SHA1 Message Date
eb71fda356 0.4.4 -> 0.4.5 2023-08-15 22:56:43 +03:00
cc400d5a12 0.4.3 -> 0.4.4 2023-08-15 22:53:42 +03:00
67b3bd26bb 0.4.3 -> 0.4.4 2023-08-15 22:53:20 +03:00
77250561fb Update build 2023-08-15 22:35:42 +03:00
70dfb9b40c Move translations 2023-08-15 22:34:27 +03:00
3118c74990 Update docs 2023-08-15 22:34:07 +03:00
cb6adde7c2 prepare for .exe builds 2023-08-15 22:15:19 +03:00
422dd35a8f prepare for .exe build 2023-08-15 21:51:57 +03:00
2c2bd1cb4a n\f event fix 2023-08-15 19:31:39 +03:00
7eba3d5877 mod time fix 2023-08-15 16:56:41 +03:00
abbd64184e t fix 2023-07-31 22:33:51 +03:00
e5dd63579b RCON (WIP) 2023-07-31 21:38:36 +03:00
ef286b7e03 RCON (WIP) 2023-07-31 21:38:08 +03:00
3a42fa13e7 Work time 2023-07-31 21:37:48 +03:00
cdec0b9949 Minor fixes 2023-07-31 21:37:15 +03:00
de91d075b4 Protocol fixes 2023-07-27 02:05:49 +03:00
a7c02e0b52 Added kick command 2023-07-26 22:38:50 +03:00
6dd3de63a9 Added support async command 2023-07-26 22:37:32 +03:00
7e0c50a04e Update TODOs 2023-07-26 18:42:21 +03:00
f1ab07d49a Small fixes 2023-07-26 18:41:37 +03:00
cdeacc16bf Final of i18n 2023-07-26 18:40:44 +03:00
719e705bab Start adding RCON part! 2023-07-26 05:00:49 +03:00
21dd23cb55 Added Plugins loaders translations. 2023-07-26 04:28:57 +03:00
9295ed2b7a hotfix 2023-07-26 04:25:25 +03:00
7466f987ac hotfix 2023-07-26 01:52:08 +03:00
d29bb9de98 Ready Core translations! 2023-07-26 01:51:23 +03:00
189bd0cc30 Update TODOs 2023-07-26 01:21:17 +03:00
53dae25fcf Change Client events handler 2023-07-26 01:21:01 +03:00
e241a7da4c Added Client events handler 2023-07-26 01:20:05 +03:00
13be12a7a1 Added event onCarChanged; 2023-07-26 01:09:28 +03:00
e348ffecc3 Fix lua colors 2023-07-26 00:56:49 +03:00
336aa31732 Added new translations;
Added new event;
2023-07-26 00:39:53 +03:00
d8c667ff51 Added new translations;
Added new event;
2023-07-26 00:32:11 +03:00
658f9ed9c6 Add some colors ^) 2023-07-25 21:19:13 +03:00
c1cb8dcdba Minor update 2023-07-25 21:19:00 +03:00
f52c73ab76 Non CMD mode. 2023-07-25 19:28:49 +03:00
6d01f0ef8d build and ver update 2023-07-24 16:17:30 +03:00
eea03c835f prompt fix 2023-07-24 16:12:35 +03:00
f28a783f7e Update About 2023-07-24 16:03:56 +03:00
dbb27ff6d5 Update About 2023-07-24 16:02:40 +03:00
e621c8dc7c lua event onConsoleInput 2023-07-24 15:37:26 +03:00
76b568c248 Update TODOs 2023-07-24 05:17:23 +03:00
4c7f5ac14b Update TODOs 2023-07-24 05:16:25 +03:00
28386a0300 Update TODOs 2023-07-24 05:14:46 +03:00
8140b3347a Update docs 2023-07-24 05:11:58 +03:00
38 changed files with 1164 additions and 558 deletions

3
.gitignore vendored
View File

@@ -140,3 +140,6 @@ dmypy.json
logs/ logs/
*.yml *.yml
*.toml *.toml
/win-ver_info.txt
/output/

View File

@@ -4,9 +4,16 @@
**_[Status: Beta]_** \ **_[Status: Beta]_** \
BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients. BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
Why did I decide to write my own kernel from scratch?\
I didn't like writing plugins in Lua after using Python; it was very inconvenient, there were always some nuances, which ultimately led me to create KuiToi!
**Our site**: [kuitoi.su](https://kuitoi.su) (WIP)\
**Our forum**: [forum.kuitoi.su](https://forum.kuitoi.su) (WIP)\
**Our discord**: [KuiToi](https://discord.gg/BAcgaAmdkJ)
## TODOs ## TODOs
- [x] Server core - [x] Server core:
- [x] BeamMP System: - [x] BeamMP System:
- [x] Private access (Without key, Direct connect) - [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher) - [x] Public access (With key, listing in Launcher)
@@ -19,25 +26,21 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Chat - [x] Chat
- [x] Players online counter - [x] Players online counter
- [x] Packets handled (Recursive finding second packet) - [x] Packets handled (Recursive finding second packet)
- [ ] Client events - [x] Client events
- [x] Car synchronizations: - [x] Car synchronizations:
- [x] State packets - [x] State packets
- [x] Spawn cars - [x] Spawn cars
- [x] Delete cars - [x] Delete cars
- [x] Edit cars - [x] Edit cars
- [x] Reset cars - [x] Reset cars
- [x] "ABG:" (compressed data) - [x] "ABG": (compressed data)
- [x] Decompress data - [x] Decompress data
- [x] Compress data - [x] Compress data
- [x] UDP Server part: - [x] UDP Server part:
- [x] Ping - [x] Ping
- [x] Position synchronizations - [x] Position synchronizations
- [x] Additional: - [x] Additional:
- [ ] KuiToi System - [x] Logger:
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [x] Logger
- [x] Just logging - [x] Just logging
- [x] Log in file - [x] Log in file
- [x] Log history (.1.log, .2.log, ...) - [x] Log history (.1.log, .2.log, ...)
@@ -45,13 +48,17 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] Tabulation - [x] Tabulation
- [x] History - [x] History
- [x] Autocomplete - [x] Autocomplete
- [x] Events System - [x] Events System:
- [x] Call events - [x] Call events
- [x] Create custom events - [x] Create custom events
- [x] Return from events - [x] Return from events
- [x] Async support - [x] Async support
- [ ] Add all events - [x] Add all events
- [x] Plugins supports - [x] MultiLanguage: (i18n support)
- [x] Core
- [x] Console
- [x] WebAPI
- [x] Plugins supports:
- [x] Python part: - [x] Python part:
- [x] Load Python plugins - [x] Load Python plugins
- [x] Async support - [x] Async support
@@ -62,16 +69,21 @@ BeamingDrive Multiplayer (BeamMP) server compatible with BeamMP clients.
- [x] MP Class - [x] MP Class
- [x] Util class - [x] Util class
- [x] FS class - [x] FS class
- [x] MultiLanguage (i18n support) - [ ] HTTP API Server: (fastapi)
- [ ] Core
- [x] Console
- [x] WebAPI
- [ ] HTTP API Server (fastapi)
- [x] Stop and Start with core - [x] Stop and Start with core
- [x] Configure FastAPI logger - [x] Configure FastAPI logger
- [ ] Sync with event system - [ ] Sync with event system
- [ ] Add methods... - [ ] Add methods...
- [ ] [Documentation](./docs/) - [ ] RCON System:
- [x] Serving
- [ ] Handle commands
- [x] Client
- [x] AES encryption
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] [Documentation](./docs)
## Installation ## Installation

View File

@@ -1,3 +1,3 @@
# MultiLanguage - i18n支持 # MultiLanguage - i18n支持
在 [example.json](./example.json) 中是 [src/modules/i18n/files/ru.json](../../../src/modules/i18n/files/ru.json) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。 在 [example.json](./example.json) 中是 [src/modules/i18n/files/ru.json](../../../src/translates/ru.json) 的副本。如果你想将其翻译成以前未翻译过的语言,或者更新现有的翻译,我将很高兴接受你的拉取请求。

View File

@@ -11,7 +11,6 @@ KiuToi几乎完全支持BeamMP的lua插件所有必要的方法都已经创
#### Cobalt Essentials V1.7.5(免费,[github ↗](https://github.com/prestonelam2003/CobaltEssentials/) #### Cobalt Essentials V1.7.5(免费,[github ↗](https://github.com/prestonelam2003/CobaltEssentials/)
1. 要获取`pluginPath`,需要:`debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")`,因为路径返回值中包含`@`,这破坏了插件。 1. 要获取`pluginPath`,需要:`debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")`,因为路径返回值中包含`@`,这破坏了插件。
2. `cobaltSysChar`
### 工作原理 ### 工作原理

View File

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

View File

@@ -11,7 +11,6 @@ KiuToi does not support: `MP.Set()`
#### Cobalt Essentials V1.7.5 (Free, [github](https://github.com/prestonelam2003/CobaltEssentials/)) #### Cobalt Essentials V1.7.5 (Free, [github](https://github.com/prestonelam2003/CobaltEssentials/))
1. To obtain `pluginPath`, use: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` as the path returns with `@`, which broke the plugin. 1. To obtain `pluginPath`, use: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` as the path returns with `@`, which broke the plugin.
2. `cobaltSysChar`
### A Little About How it Works ### A Little About How it Works

View File

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

View File

@@ -11,7 +11,6 @@
#### Cobalt Essentials V1.7.5 (Бесплатный, [github](https://github.com/prestonelam2003/CobaltEssentials/)) #### Cobalt Essentials V1.7.5 (Бесплатный, [github](https://github.com/prestonelam2003/CobaltEssentials/))
1. Для получения `pluginPath` нужно: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` так как пусть возвращается с `@`, что сломало плагин. 1. Для получения `pluginPath` нужно: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` так как пусть возвращается с `@`, что сломало плагин.
2. `cobaltSysChar`
### Немного о принципе работы ### Немного о принципе работы

View File

@@ -7,4 +7,6 @@ starlette~=0.27.0
pydantic~=2.0.2 pydantic~=2.0.2
click~=8.1.4 click~=8.1.4
lupa~=2.0 lupa~=2.0
toml~=0.10.2 toml~=0.10.2
colorama~=0.4.6
cryptography~=41.0.2

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -44,6 +44,10 @@ class Client:
def _writer(self): def _writer(self):
return self.__writer return self.__writer
@property
def alive(self):
return self.__alive
@property @property
def log(self): def log(self):
return self._log return self._log
@@ -111,8 +115,7 @@ class Client:
if not self.__alive: if not self.__alive:
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;") self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
return return
# TODO: i18n self.log.info(i18n.client_kicked.format(reason))
self.log.info(f"Kicked with reason: \"{reason}\"")
await self._send(f"K{reason}") await self._send(f"K{reason}")
self.__alive = False self.__alive = False
@@ -186,8 +189,8 @@ class Client:
await writer.drain() await writer.drain()
return True return True
except ConnectionError: except Exception as e:
self.log.debug('[TCP] Disconnected') self.log.debug(f'[TCP] Disconnected: {e}')
self.__alive = False self.__alive = False
await self._remove_me() await self._remove_me()
return False return False
@@ -195,7 +198,13 @@ class Client:
async def _recv(self, one=False): async def _recv(self, one=False):
while self.__alive: while self.__alive:
try: try:
header = await self.__reader.read(4) header = b""
while len(header) < 4 and self.__alive:
h = await self.__reader.read(4)
if not h:
break
else:
header += h
int_header = int.from_bytes(header, byteorder='little', signed=True) int_header = int.from_bytes(header, byteorder='little', signed=True)
@@ -220,9 +229,14 @@ class Client:
self.__packets_queue.append(None) self.__packets_queue.append(None)
continue continue
data = await self.__reader.read(int_header) data = b""
while len(data) < int_header and self.__alive:
buffer = await self.__reader.read(int_header - len(data))
if not buffer:
break
else:
data += buffer
# self.log.debug(f"int_header: {int_header}; data: `{data}`;")
abg = b"ABG:" abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg): if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):]) data = zlib.decompress(data[len(abg):])
@@ -251,7 +265,7 @@ class Client:
try: try:
writer.write(data) writer.write(data)
await writer.drain() await writer.drain()
self.log.debug(f"[{who}] Sent {len(data)} bytes.") # self.log.debug(f"[{who}] Sent {len(data)} bytes.")
except ConnectionError: except ConnectionError:
self.__alive = False self.__alive = False
self.log.debug(f"[{who}] Disconnected.") self.log.debug(f"[{who}] Disconnected.")
@@ -270,10 +284,12 @@ class Client:
async def _sync_resources(self): async def _sync_resources(self):
while self.__alive: while self.__alive:
data = await self._recv(True) data = await self._recv(True)
if data is None:
await self._remove_me()
break
if data.startswith(b"f"): if data.startswith(b"f"):
file = data[1:].decode(config.enc) file = data[1:].decode(config.enc)
# TODO: i18n self.log.info(i18n.client_mod_request.format(repr(file)))
self.log.info(f"Requested mode: {file!r}")
size = -1 size = -1
for mod in self.__Core.mods_list: for mod in self.__Core.mods_list:
if type(mod) == int: if type(mod) == int:
@@ -309,13 +325,12 @@ class Client:
self._split_load(half_size, size, True, file, speed) self._split_load(half_size, size, True, file, speed)
] ]
sl0, sl1 = await asyncio.gather(*uploads) sl0, sl1 = await asyncio.gather(*uploads)
tr = time.monotonic() - t tr = (time.monotonic() - t) or 0.0001
if self.__Core.lock_upload: if self.__Core.lock_upload:
self.__Core.lock_upload = False self.__Core.lock_upload = False
# TODO: i18n msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
if speed: if speed:
msg += f" of limit {int(speed * 2)}Mb/s" msg += i18n.client_mod_sent_limit.format(int(speed * 2))
self.log.info(msg) self.log.info(msg)
sent = sl0 + sl1 sent = sl0 + sl1
ok = sent == size ok = sent == size
@@ -323,8 +338,7 @@ class Client:
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}") self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
if not ok: if not ok:
self.__alive = False self.__alive = False
# TODO: i18n self.log.error(i18n.client_mod_sent_error.format(repr(file)))
self.log.error(f"Error while sending: {file!r}")
return return
elif data.startswith(b"SR"): elif data.startswith(b"SR"):
path_list = '' path_list = ''
@@ -351,7 +365,7 @@ class Client:
id_sep = s.find('-') id_sep = s.find('-')
if id_sep == -1: if id_sep == -1:
self.log.debug( self.log.debug(
f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}'") f"Invalid packet: Could not parse pid/vid from packet, as there is no '-' separator: '{data}', {s}")
return -1, -1 return -1, -1
cid = s[:id_sep] cid = s[:id_sep]
vid = s[id_sep + 1:] vid = s[id_sep + 1:]
@@ -385,10 +399,11 @@ class Client:
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):]) lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
if 1 in lua_data: if 1 in lua_data:
allow = False allow = False
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self) ev_data_list = ev.call_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self) d2 = await ev.call_async_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
ev_data_list.extend(d2) ev_data_list.extend(d2)
for ev_data in ev_data_list: for ev_data in ev_data_list:
self.log.debug(ev_data)
# TODO: handle event onCarSpawn # TODO: handle event onCarSpawn
pass pass
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}" pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
@@ -435,10 +450,11 @@ class Client:
ev.call_lua_event("onVehicleDeleted", self.cid, car_id) ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
admin_allow = False # Delete from admin, for example... 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) ev_data_list = ev.call_event("onCarDelete", data=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) d2 = await ev.call_async_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
ev_data_list.extend(d2) ev_data_list.extend(d2)
for ev_data in ev_data_list: for ev_data in ev_data_list:
self.log.debug(ev_data)
# TODO: handle event onCarDelete # TODO: handle event onCarDelete
pass pass
@@ -474,10 +490,11 @@ class Client:
lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):]) lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):])
if 1 in lua_data: if 1 in lua_data:
allow = False allow = False
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self) ev_data_list = ev.call_event("onCarEdited", data=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) d2 = await ev.call_async_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
ev_data_list.extend(d2) ev_data_list.extend(d2)
for ev_data in ev_data_list: for ev_data in ev_data_list:
self.log.debug(ev_data)
# TODO: handle event onCarEdited # TODO: handle event onCarEdited
pass pass
@@ -498,6 +515,12 @@ class Client:
else: else:
self.log.debug(f"Invalid car: car_id={car_id}") self.log.debug(f"Invalid car: car_id={car_id}")
async def reset_car(self, car_id, x, y, z, rot=None):
# TODO: reset_car
self.log.debug(f"Resetting car from plugin")
if rot is None:
rot = {"y": 0, "w": 0, "x": 0, "z": 0}
async def _reset_car(self, raw_data): async def _reset_car(self, raw_data):
cid, car_id = self._get_cid_vid(raw_data) cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self._cars[car_id]: if car_id != -1 and cid == self.cid and self._cars[car_id]:
@@ -508,8 +531,8 @@ class Client:
car_json = json.loads(raw_data[raw_data.find("{"):]) car_json = json.loads(raw_data[raw_data.find("{"):])
except Exception as e: except Exception as e:
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}") self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}")
ev.call_event("onCarReset", car=car_json, car_id=car_id, player=self) ev.call_event("onCarReset", data=car_json, car_id=car_id, player=self)
await ev.call_async_event("onCarReset", car=car_json, car_id=car_id, player=self) await ev.call_async_event("onCarReset", data=car_json, car_id=car_id, player=self)
self.log.debug(f"Car reset: car_id={car_id}") self.log.debug(f"Car reset: car_id={car_id}")
else: else:
self.log.debug(f"Invalid car: car_id={car_id}") self.log.debug(f"Invalid car: car_id={car_id}")
@@ -539,23 +562,30 @@ class Client:
case "t": # Broken details case "t": # Broken details
self.log.debug(f"Something changed/broken: {raw_data}") self.log.debug(f"Something changed/broken: {raw_data}")
cid, car_id = self._get_cid_vid(raw_data)
if car_id != -1 and cid == self.cid and self._cars[car_id]:
data = raw_data[raw_data.find("{"):]
ev.call_event("onCarChanged", car_id=car_id, data=data)
await ev.call_async_event("onCarChanged", car_id=car_id, data=data)
await self._send(raw_data, to_all=True, to_self=False) await self._send(raw_data, to_all=True, to_self=False)
case "m": # Move focus car case "m": # Move focus car
self.log.debug(f"Move focus to: {raw_data}") self.log.debug(f"Move focus to: {raw_data}")
cid, car_id = self._get_cid_vid(raw_data[5:]) cid, car_id = self._get_cid_vid(raw_data[3:])
if car_id != -1 and cid == self.cid and self._cars[car_id]: if car_id != -1 and cid == self.cid and self._cars[car_id]:
self._focus_car = car_id self._focus_car = car_id
data = raw_data[raw_data.find("{"):]
ev.call_event("onCarFocusMove", car_id=car_id, data=data)
await ev.call_async_event("onCarFocusMove", car_id=car_id, data=data)
await self._send(raw_data, to_all=True, to_self=True) await self._send(raw_data, to_all=True, to_self=True)
async def _connected_handler(self): async def _connected_handler(self):
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
# Client connected # Client connected
ev.call_event("onPlayerJoin", player=self) ev.call_event("onPlayerJoin", player=self)
await ev.call_async_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"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 await self._send(f"J{i18n.game_welcome_message.format(self.nick)}", to_all=True) # Hello message
for client in self.__Core.clients: for client in self.__Core.clients:
if not client: if not client:
@@ -565,6 +595,7 @@ class Client:
continue continue
await self._send(car['packet']) await self._send(car['packet'])
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
self._ready = True self._ready = True
async def _chat_handler(self, data): async def _chat_handler(self, data):
@@ -604,7 +635,7 @@ class Client:
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer) await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
need_send = False need_send = False
except KeyError | AttributeError: except KeyError | AttributeError:
self.log.error(f"Returns invalid data: {ev_data}") self.log.error(i18n.client_event_invalid_data.format(ev_data))
if need_send: if need_send:
if config.Options['log_chat']: if config.Options['log_chat']:
self.log.info(f"{self.nick}: {msg}") self.log.info(f"{self.nick}: {msg}")
@@ -620,11 +651,13 @@ class Client:
await self._send(data, to_all=True, to_self=False) await self._send(data, to_all=True, to_self=False)
return return
_bytes = False
try: try:
data = data.decode() data = data.decode()
except UnicodeDecodeError: except UnicodeDecodeError:
_bytes = True
self.log.error(f"UnicodeDecodeError: {data}") self.log.error(f"UnicodeDecodeError: {data}")
return self.log.info("Some things are skipping...")
# Codes: p, Z in udp_server.py # Codes: p, Z in udp_server.py
match data[0]: # At data[0] code match data[0]: # At data[0] code
@@ -632,15 +665,31 @@ class Client:
await self._connected_handler() await self._connected_handler()
case "C": # Chat handler case "C": # Chat handler
if _bytes:
return
await self._chat_handler(data) await self._chat_handler(data)
case "O": # Cars handler case "O": # Cars handler
if _bytes:
return
await self._handle_car_codes(data) await self._handle_car_codes(data)
case "E": # Client events handler case "E": # Client events handler
# TODO: Handle events from client if len(data) < 2:
pass self.log.debug("Tried to send an empty event, ignoring.")
return
if _bytes:
sep = data.find(b":", 2)
self.log.warning("Bytes event!")
else:
sep = data.find(":", 2)
if sep == -1:
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
event_name = data[2:sep]
even_data = data[sep + 1:]
ev.call_lua_event(event_name, even_data)
ev.call_event(event_name, data=even_data, player=self)
await ev.call_async_event(event_name, data=even_data, player=self)
case "N": case "N":
await self._send(data, to_all=True, to_self=False) await self._send(data, to_all=True, to_self=False)
@@ -681,8 +730,11 @@ class Client:
ev.call_event("onPlayerDisconnect", player=self) ev.call_event("onPlayerDisconnect", player=self)
await ev.call_async_event("onPlayerDisconnect", player=self) await ev.call_async_event("onPlayerDisconnect", player=self)
# TODO: i18n self.log.info(
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.") i18n.client_player_disconnected.format(
round((time.monotonic() - self._connect_time) / 60, 2)
)
)
self.__Core.clients[self.cid] = None self.__Core.clients[self.cid] = None
del self.__Core.clients_by_id[self.cid] del self.__Core.clients_by_id[self.cid]
del self.__Core.clients_by_nick[self.nick] del self.__Core.clients_by_nick[self.nick]

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -35,13 +35,15 @@ class Client:
self._ready = False self._ready = False
self._focus_car = -1 self._focus_car = -1
self._identifiers = [] self._identifiers = []
self._cars: List[Optional[Dict[str, int]]] = [] self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""} self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._last_position = {} self._last_position = {}
async def __gracefully_kick(self): ... async def __gracefully_kick(self): ...
@property @property
def _writer(self) -> StreamWriter: ... def _writer(self) -> StreamWriter: ...
@property @property
def alive(self) -> bool: ...
@property
def log(self) -> Logger: ... def log(self) -> Logger: ...
@property @property
def addr(self) -> Tuple[str, int]: ... def addr(self) -> Tuple[str, int]: ...

View File

@@ -1,8 +1,8 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.__init__.py # File core.__init__.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.3 # Version 1.5
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai) # Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
@@ -10,8 +10,8 @@
__title__ = 'KuiToi-Server' __title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.' __description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server' __url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.4.1' __version__ = '0.4.5'
__build__ = 1925 # Я это считаю лог файлами __build__ = 2300 # Я это считаю лог файлами
__author__ = 'SantaSpeen' __author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su' __author_email__ = 'admin@kuitoi.su'
__license__ = "FPA" __license__ = "FPA"
@@ -30,7 +30,7 @@ from modules import ConfigProvider, EventsSystem
from modules import Console from modules import Console
from modules import MultiLanguage from modules import MultiLanguage
args = parser.parse_args() args, _ = parser.parse_known_args()
if args.version: if args.version:
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}") print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
exit(0) exit(0)
@@ -102,7 +102,7 @@ if not config.Auth['private'] and not config.Auth['key']:
log.debug("Initializing console...") log.debug("Initializing console...")
console = Console() console = Console()
console.builtins_hook() console.builtins_hook()
# console.logger_hook() console.logger_hook()
console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop) console.add_command("stop", console.stop, i18n.man_message_stop, i18n.help_message_stop)
console.add_command("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit) console.add_command("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit)

View File

@@ -1,18 +1,20 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.core.py # File core.core.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version: 0.4.1 # Version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
import math
import os import os
import random import random
import time
from threading import Thread from threading import Thread
import aiohttp import aiohttp
import uvicorn import uvicorn
from core import utils from core import utils, __version__
from core.Client import Client from core.Client import Client
from core.tcp_server import TCPServer from core.tcp_server import TCPServer
from core.udp_server import UDPServer from core.udp_server import UDPServer
@@ -26,6 +28,7 @@ class Core:
def __init__(self): def __init__(self):
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.start_time = time.monotonic()
self.run = False self.run = False
self.direct = False self.direct = False
self.clients = [] self.clients = []
@@ -111,6 +114,8 @@ class Core:
if not client.ready: if not client.ready:
client.is_disconnected() client.is_disconnected()
continue continue
if not client.alive:
await client.kick("You are not alive!")
await client._send(ca) await client._send(ca)
except Exception as e: except Exception as e:
self.log.error("Error in check_alive.") self.log.error("Error in check_alive.")
@@ -142,8 +147,7 @@ class Core:
async def heartbeat(self, test=False): async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct: if config.Auth["private"] or self.direct:
if test: if test:
# TODO: i18n self.log.info(i18n.core_direct_mode)
self.log.info(f"Server runnig in Direct connect mode.")
self.direct = True self.direct = True
return return
@@ -184,31 +188,30 @@ class Core:
if not (body.get("status") is not None and if not (body.get("status") is not None and
body.get("code") is not None and body.get("code") is not None and
body.get("msg") is not None): body.get("msg") is not None):
self.log.error("Missing/invalid json members in backend response") self.log.error(i18n.core_auth_server_error)
raise KeyboardInterrupt return
status = body.get("status") status = body.get("status")
msg = body.get("msg") msg = body.get("msg")
if status == "2000": if status == "2000":
if test: if test:
# TODO: i18n self.log.debug(f"Authenticated! {msg}")
self.log.info(f"Authenticated! {msg}")
elif status == "200": elif status == "200":
if test: if test:
self.log.info(f"Resumed authenticated session. {msg}") self.log.debug(f"Resumed authenticated session. {msg}")
else: else:
self.log.debug(f"Auth: data {data}") self.log.debug(f"Auth: data {data}")
self.log.debug(f"Auth: code {code}, body {body}") self.log.debug(f"Auth: code {code}, body {body}")
self.log.error(f"Backend REFUSED the auth key. Reason: "
f"{msg or 'Backend did not provide a reason'}") self.log.error(i18n.core_auth_server_refused.format(
self.log.info(f"Server still runnig, but only in Direct connect mode.") msg or i18n.core_auth_server_refused_no_reason))
self.log.info(i18n.core_auth_server_refused_direct_node)
self.direct = True self.direct = True
else: else:
self.direct = True self.direct = True
if test: if test:
# TODO: i18n self.log.error(i18n.core_auth_server_no_response)
self.log.error("Cannot authenticate server.") self.log.info(i18n.core_auth_server_refused_direct_node)
self.log.info(f"Server still runnig, but only in Direct connect mode.")
# if not config.Auth['private']: # if not config.Auth['private']:
# raise KeyboardInterrupt # raise KeyboardInterrupt
@@ -219,6 +222,22 @@ class Core:
except Exception as e: except Exception as e:
self.log.error(f"Error in heartbeat: {e}") self.log.error(f"Error in heartbeat: {e}")
async def kick_cmd(self, args):
if not len(args) > 0:
return "\nUsage: kick <nick>|:<id> [reason]\nExamples:\n\tkick admin bad boy\n\tkick :0 bad boy"
reason = "kicked by console."
if len(args) > 1:
reason = " ".join(args[1:])
cl = args[0]
if cl.startswith(":") and cl[1:].isdigit():
client = self.get_client(cid=int(cl[1:]))
else:
client = self.get_client(nick=cl)
if client:
await client.kick(reason)
else:
return "Client not found."
async def main(self): async def main(self):
self.tcp = self.tcp(self, self.server_ip, self.server_port) self.tcp = self.tcp(self, self.server_ip, self.server_port)
self.udp = self.udp(self, self.server_ip, self.server_port) self.udp = self.udp(self, self.server_ip, self.server_port)
@@ -226,6 +245,7 @@ class Core:
"list", "list",
lambda x: f"Players list: {self.get_clients_list(True)}" lambda x: f"Players list: {self.get_clients_list(True)}"
) )
console.add_command("kick", self.kick_cmd)
pl_dir = "plugins" pl_dir = "plugins"
self.log.debug("Initializing PluginsLoaders...") self.log.debug("Initializing PluginsLoaders...")
@@ -263,8 +283,7 @@ class Core:
self.log.debug(f"mods_list: {self.mods_list}") self.log.debug(f"mods_list: {self.mods_list}")
len_mods = len(self.mods_list) - 1 len_mods = len(self.mods_list) - 1
if len_mods > 0: if len_mods > 0:
# TODO: i18n self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
self.log.info(i18n.init_ok) self.log.info(i18n.init_ok)
await self.heartbeat(True) await self.heartbeat(True)
@@ -273,12 +292,15 @@ class Core:
tasks = [] tasks = []
# self.udp.start, # self.udp.start,
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive] f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
if config.RCON['enabled']:
console.rcon.version = f"KuiToi {__version__}"
rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port'])
f_tasks.append(rcon.start)
for task in f_tasks: for task in f_tasks:
tasks.append(asyncio.create_task(task())) tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
await ev.call_async_event("_plugins_start") await ev.call_async_event("_plugins_start")
# await ev.call_async_event("_lua_plugins_start")
self.run = True self.run = True
self.log.info(i18n.start) self.log.info(i18n.start)
@@ -304,10 +326,16 @@ class Core:
ev.call_event("onServerStopped") ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped") await ev.call_async_event("onServerStopped")
await self.__gracefully_kick() await self.__gracefully_kick()
if config.Options['use_lua']:
ev.call_event("_lua_plugins_unload")
await ev.call_async_event("_plugins_unload") await ev.call_async_event("_plugins_unload")
ev.call_event("_lua_plugins_unload")
self.run = False self.run = False
self.log.info(i18n.stop)
if config.WebAPI["enabled"]: if config.WebAPI["enabled"]:
asyncio.run(self.web_stop()) asyncio.run(self.web_stop())
# exit(0) total_time = time.monotonic() - self.start_time
hours = int(total_time // 3600)
minutes = int((total_time % 3600) // 60)
seconds = math.ceil(total_time % 60)
t = f"{'' if not hours else f'{hours} hours, '}{'' if not hours else f'{minutes} min., '}{seconds} sec."
self.log.info(f"Working time: {t}")
self.log.info(i18n.stop)

View File

@@ -1,10 +1,11 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.core.pyi # File core.core.pyi
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
import time
from threading import Thread from threading import Thread
from typing import Callable, List, Dict from typing import Callable, List, Dict
@@ -16,6 +17,7 @@ from .udp_server import UDPServer
class Core: class Core:
def __init__(self): def __init__(self):
self.start_time = time.monotonic()
self.log = utils.get_logger("core") self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.run = False self.run = False
@@ -45,6 +47,7 @@ class Core:
def start_web() -> None: ... def start_web() -> None: ...
def stop_me(self) -> None: ... def stop_me(self) -> None: ...
async def heartbeat(self, test=False) -> None: ... async def heartbeat(self, test=False) -> None: ...
async def kick_cmd(self, args: list) -> None | str: ...
async def main(self) -> None: ... async def main(self) -> None: ...
def start(self) -> None: ... def start(self) -> None: ...
async def stop(self) -> None: ... async def stop(self) -> None: ...

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.py # File core.tcp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -24,13 +24,11 @@ class TCPServer:
async def auth_client(self, reader, writer): async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer) client = self.Core.create_client(reader, writer)
# TODO: i18n self.log.info(i18n.core_identifying_connection)
self.log.info(f"Identifying new ClientConnection...")
data = await client._recv(True) data = await client._recv(True)
self.log.debug(f"Version: {data}") self.log.debug(f"Version: {data}")
if data.decode("utf-8") != f"VC{self.Core.client_major_version}": if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
# TODO: i18n await client.kick(i18n.core_player_kick_outdated)
await client.kick("Outdated Version.")
return False, client return False, client
else: else:
await client._send(b"S") # Accepted client version await client._send(b"S") # Accepted client version
@@ -38,8 +36,7 @@ class TCPServer:
data = await client._recv(True) data = await client._recv(True)
self.log.debug(f"Key: {data}") self.log.debug(f"Key: {data}")
if len(data) > 50: if len(data) > 50:
# TODO: i18n await client.kick(i18n.core_player_kick_bad_key)
await client.kick("Invalid Key (too long)!")
return False, client return False, client
client._key = data.decode("utf-8") client._key = data.decode("utf-8")
ev.call_event("onPlayerSentKey", player=client) ev.call_event("onPlayerSentKey", player=client)
@@ -50,8 +47,7 @@ class TCPServer:
res = await response.json() res = await response.json()
self.log.debug(f"res: {res}") self.log.debug(f"res: {res}")
if res.get("error"): if res.get("error"):
# TODO: i18n await client.kick(i18n.core_player_kick_invalid_key)
await client.kick('Invalid key! Please restart your game.')
return False, client return False, client
client.nick = res["username"] client.nick = res["username"]
client.roles = res["roles"] client.roles = res["roles"]
@@ -60,21 +56,18 @@ class TCPServer:
# noinspection PyProtectedMember # noinspection PyProtectedMember
client._update_logger() client._update_logger()
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error(f"Auth error: {e}") self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.') await client.kick(i18n.core_player_kick_auth_server_fail)
return False, client return False, client
for _client in self.Core.clients: for _client in self.Core.clients:
if not _client: if not _client:
continue continue
if _client.nick == client.nick and _client.guest == client.guest: if _client.nick == client.nick and _client.guest == client.guest:
# TODO: i18n await _client.kick(i18n.core_player_kick_stale)
await client.kick('Stale Client (replaced by new client)')
return False, client
allow = True allow = True
reason = "You are not allowed on the server!" reason = i18n.core_player_kick_no_allowed_default_reason
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers) lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
for data in lua_data: for data in lua_data:
@@ -90,13 +83,12 @@ class TCPServer:
ev.call_event("onPlayerAuthenticated", player=client) ev.call_event("onPlayerAuthenticated", player=client)
if len(self.Core.clients_by_id) > config.Game["players"]: if len(self.Core.clients_by_id) > config.Game["players"]:
# TODO: i18n await client.kick(i18n.core_player_kick_server_full)
await client.kick("Server full!")
return False, client return False, client
else: else:
# TODO: i18n self.log.info(i18n.core_identifying_okay)
self.log.info("Identification success")
await self.Core.insert_client(client) await self.Core.insert_client(client)
client.log.info(i18n.core_player_set_id.format(client.pid))
return True, client return True, client
@@ -127,8 +119,8 @@ class TCPServer:
await writer.drain() await writer.drain()
writer.close() writer.close()
case _: case _:
# TODO: i18n
self.log.error(f"Unknown code: {code}") self.log.error(f"Unknown code: {code}")
self.log.info("Report about that!")
writer.close() writer.close()
return False, None return False, None
@@ -148,7 +140,6 @@ class TCPServer:
del cl del cl
break break
except Exception as e: except Exception as e:
# TODO: i18n
self.log.error("Error while handling connection...") self.log.error("Error while handling connection...")
self.log.exception(e) self.log.exception(e)
traceback.print_exc() traceback.print_exc()
@@ -165,8 +156,7 @@ class TCPServer:
async with server: async with server:
await server.serve_forever() await server.serve_forever()
except OSError as e: except OSError as e:
# TODO: i18n self.log.error(i18n.core_bind_failed.format(e))
self.log.error("Cannot bind port")
raise e raise e
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.tcp_server.pyi # File core.tcp_server.pyi
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.udp_server.py # File core.udp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio
@@ -100,8 +100,8 @@ class UDPServer(asyncio.DatagramTransport):
await asyncio.sleep(0.2) await asyncio.sleep(0.2)
except OSError as e: except OSError as e:
self.run = False # self.run = False
self.Core.run = False # self.Core.run = False
self.log.error(f"Cannot bind port or other error: {e}") self.log.error(f"Cannot bind port or other error: {e}")
except Exception as e: except Exception as e:
self.log.error(f"Error: {e}") self.log.error(f"Error: {e}")

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev # Developed by KuiToi Dev
# File core.udp_server.py # File core.udp_server.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import asyncio import asyncio

View File

@@ -2,7 +2,7 @@
# File core.utils.py # File core.utils.py
# Written by: SantaSpeen # Written by: SantaSpeen
# Version 1.1 # Version 1.1
# Core version: 0.4.1 # Core version: 0.4.5
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import datetime import datetime

View File

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

View File

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

View File

@@ -0,0 +1,186 @@
import asyncio
import binascii
import hashlib
import os
import zlib
from base64 import b64decode, b64encode
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from core import get_logger
class RCONSystem:
console = None
version = "verError"
def __init__(self, key, host, port):
self.log = get_logger("RCON")
self.key = hashlib.sha256(key.encode(config.enc)).digest()
self.host = host
self.port = port
self.run = False
def _encrypt(self, message):
self.log.debug(f"Encrypt message: {message}")
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv))
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(message) + padder.finalize()
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
encoded_data = b64encode(zlib.compress(encrypted_data, level=zlib.Z_BEST_COMPRESSION))
encoded_iv = b64encode(iv)
return encoded_iv + b":" + encoded_data
def _decrypt(self, ciphertext):
self.log.debug(f"Decrypt message: {ciphertext}")
encoded_iv, encoded_data = ciphertext.split(b":", 2)
iv = b64decode(encoded_iv)
encrypted_data = zlib.decompress(b64decode(encoded_data))
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv))
decryptor = cipher.decryptor()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
return unpadded_data
async def _recv(self, reader, writer) -> tuple[str, bool]:
try:
header = b""
while len(header) < 4:
h = await reader.read(4 - len(header))
if not h:
break
else:
header += h
header = int.from_bytes(header, byteorder='little', signed=True)
if header <= 0:
self.log.warning("Connection closed!")
writer.close()
encrypted_data = b""
while len(encrypted_data) < header:
buffer = await reader.read(header - len(encrypted_data))
if not buffer:
break
else:
encrypted_data += buffer
try:
data, s = self._decrypt(encrypted_data), True
except binascii.Error:
data, s = encrypted_data, False
except ValueError:
data, s = encrypted_data, False
self.log.debug(f"Received: {data}, {s}")
return data.decode(config.enc), s
except ConnectionResetError:
self.log.warning("Connection reset.")
return "", False
async def _send(self, data, writer, encrypt=True, warn=True):
self.log.debug(f"Sending: \"{data}\"")
if isinstance(data, str):
data = data.encode(config.enc)
if encrypt:
data = self._encrypt(data)
self.log.debug(f"Send encrypted: {data}")
header = len(data).to_bytes(4, "little", signed=True)
try:
writer.write(header + data)
await writer.drain()
return True
except ConnectionError:
self.log.debug("Sending error...")
if encrypt and warn:
self.log.warning("Connection closed!")
return False
async def send_hello(self, writer, work):
while work[0]:
await asyncio.sleep(5)
if not await self._send("Cs:hello", writer, warn=False):
work[0] = False
writer.close()
break
async def while_handle(self, reader, writer):
ver, status = await self._recv(reader, writer)
if ver == "ver" and status:
await self._send(self.version, writer)
cmds, status = await self._recv(reader, writer)
if cmds == "commands" and status:
await self._send("SKIP", writer)
work = [True]
t = asyncio.create_task(self.send_hello(writer, work))
while work[0]:
data, status = await self._recv(reader, writer)
if not status:
work[0] = False
writer.close()
break
code = data[:2]
message = data[data.find(":") + 1:]
match code:
case "Cs":
match message:
case "hello":
await self._send("Os:hello", writer)
case _:
self.log.warning(f"Unknown command: {data}")
case "C:":
self.log.info(f"Called the command: {message}")
if message == "exit":
self.log.info("Connection closed.")
writer.close()
work[0] = False
break
case "Os":
match message:
case "hello":
pass
# await self._send("Cs:hello", writer)
case _:
self.log.warning(f"Unknown command: {data}")
case "O:":
pass
case _:
self.log.warning(f"Unknown command: {data}")
await t
async def handle_connect(self, reader, writer):
try:
hello, status = await self._recv(reader, writer)
if hello == "hello" and status:
await self._send("hello", writer)
await self.while_handle(reader, writer)
else:
await self._send("E:Wrong password", writer, False)
writer.close()
except Exception as e:
self.log.error("Error while handling connection...")
self.log.exception(e)
async def start(self):
self.run = True
try:
server = await asyncio.start_server(self.handle_connect, self.host, self.port, backlog=5)
self.log.info(f"RCON server started on {server.sockets[0].getsockname()!r}")
async with server:
await server.serve_forever()
except OSError as e:
self.log.error(i18n.core_bind_failed.format(e))
raise e
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Error: {e}")
raise e
finally:
self.run = False

View File

@@ -1,36 +1,34 @@
class Console(object): from logging import Logger
from typing import AnyStr
def __init__(self, from core import get_logger
prompt_in: str = ">",
prompt_out: str = "]:",
not_found: str = "Command \"%s\" not found in alias.") -> None: ...
def __getitem__(self, item): ...
@property
def alias(self) -> dict: ...
def add(self, key: str, func: function) -> dict: ...
def log(self, s: str, r='\r') -> None: ...
def write(self, s: str, r='\r') -> None: ...
def __lshift__(self, s: AnyStr) -> None: ...
def logger_hook(self) -> None: ...
def builtins_hook(self) -> None: ...
async def start(self) -> None: ...
class console(object): class RCONSystem:
console = None
def __init__(self, key, host, port):
self.log = get_logger("RCON")
self.key = key
self.host = host
self.port = port
async def start(self): ...
async def stop(self): ...
class console:
rcon: RCONSystem = RCONSystem
@staticmethod @staticmethod
def alias() -> dict: ... def alias() -> dict: ...
@staticmethod @staticmethod
def add_command(key: str, func: function) -> dict: ... def add_command(key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: ...
@staticmethod @staticmethod
async def start() -> None: ... async def start() -> None: ...
@staticmethod @staticmethod
def builtins_hook() -> None: ... def builtins_hook() -> None: ...
@staticmethod @staticmethod
def logger_hook() -> None: ... def logger_hook() -> None: ...
@staticmethod @staticmethod
def log(s: str) -> None: ... def log(s: str) -> None: ...
@staticmethod @staticmethod

View File

@@ -7,6 +7,7 @@
# Licence: FPA # Licence: FPA
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import builtins import builtins
import inspect
import logging import logging
from typing import AnyStr from typing import AnyStr
@@ -14,9 +15,11 @@ from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger from core import get_logger
from modules.ConsoleSystem.RCON import RCONSystem
class Console: class Console:
@@ -28,6 +31,7 @@ class Console:
debug=False) -> None: debug=False) -> None:
self.__logger = get_logger("console") self.__logger = get_logger("console")
self.__is_run = False self.__is_run = False
self.no_cmd = False
self.__prompt_in = prompt_in self.__prompt_in = prompt_in
self.__prompt_out = prompt_out self.__prompt_out = prompt_out
self.__not_found = not_found self.__not_found = not_found
@@ -43,6 +47,9 @@ class Console:
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help, self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}}) custom_completer={"help": {"--raw": None}})
self.completer = NestedCompleter.from_nested_dict(self.__alias) self.completer = NestedCompleter.from_nested_dict(self.__alias)
rcon = RCONSystem
rcon.console = self
self.rcon = rcon
def __debug(self, *x): def __debug(self, *x):
self.__logger.debug(f"{x}") self.__logger.debug(f"{x}")
@@ -122,19 +129,38 @@ class Console:
self.__alias.update(custom_completer or {key: None}) self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None}) self.__alias["man"].update({key: None})
self.__func.update({key: {"f": func}}) self.__func.update({key: {"f": func}})
self.__man.update({key: f'html<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None}) self.__man.update({key: f'html:<seagreen>{i18n.man_for} <b>{key}</b>\n{man}</seagreen>' if man else None})
self.__desc.update({key: desc}) self.__desc.update({key: desc})
self.__update_completer() self.__update_completer()
return self.__alias.copy() return self.__alias.copy()
def _write(self, t):
if self.no_cmd:
print(t)
return
try:
if t.startswith("html:"):
print_formatted_text(HTML(t[5:]))
else:
print_formatted_text(t)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
print(t)
def write(self, s: AnyStr): def write(self, s: AnyStr):
if s.startswith("html"): if isinstance(s, (list, tuple)):
print_formatted_text(HTML(s[4:])) for text in s:
self._write(text)
else: else:
print_formatted_text(s) self._write(s)
def log(self, s: AnyStr) -> None: def log(self, s: AnyStr) -> None:
self.__logger.info(f"{s}") if isinstance(s, (list, tuple)):
for text in s:
self.__logger.info(f"{text}")
else:
self.__logger.info(f"{s}")
# self.write(s) # self.write(s)
def __lshift__(self, s: AnyStr) -> None: def __lshift__(self, s: AnyStr) -> None:
@@ -191,23 +217,42 @@ class Console:
while True: while True:
try: try:
with patch_stdout(): with patch_stdout():
cmd_in = await session.prompt_async( if self.no_cmd:
self.__prompt_in, cmd_in = input(self.__prompt_in)
completer=self.completer, else:
auto_suggest=AutoSuggestFromHistory() try:
) cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
except NoConsoleScreenBufferError:
print("Works in non cmd mode.")
self.no_cmd = True
cmd_s = cmd_in.split(" ") cmd_s = cmd_in.split(" ")
cmd = cmd_s[0] cmd = cmd_s[0]
if cmd == "": if cmd == "":
continue continue
else: else:
found_in_lua = False
d = ev.call_lua_event("onConsoleInput", cmd_in)
if len(d) > 0:
for text in d:
if text is not None:
found_in_lua = True
self.log(text)
command_object = self.__func.get(cmd) command_object = self.__func.get(cmd)
if command_object: if command_object:
out = command_object['f'](cmd_s[1:]) func = command_object['f']
if inspect.iscoroutinefunction(func):
out = await func(cmd_s[1:])
else:
out = func(cmd_s[1:])
if out: if out:
self.log(out) self.log(out)
else: else:
self.log(self.__not_found % cmd) if not found_in_lua:
self.log(self.__not_found % cmd)
except KeyboardInterrupt: except KeyboardInterrupt:
raise KeyboardInterrupt raise KeyboardInterrupt
except Exception as e: except Exception as e:

View File

@@ -22,19 +22,21 @@ class EventsSystem:
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
self.as_tasks = [] self.as_tasks = []
self.__events = { self.__events = {
"onServerStarted": [], "onServerStarted": [], # No handler
"onPlayerSentKey": [], # Only sync "onPlayerSentKey": [], # Only sync, no handler
"onPlayerAuthenticated": [], # Only sync "onPlayerAuthenticated": [], # (!) Only sync, With handler
"onPlayerJoin": [], "onPlayerJoin": [], # (!) With handler
"onChatReceive": [], "onChatReceive": [], # (!) With handler
"onCarSpawn": [], "onCarSpawn": [], # (!) With handler
"onCarDelete": [], "onCarDelete": [], # (!) With handler (admin allow)
"onCarEdited": [], "onCarEdited": [], # (!) With handler
"onCarReset": [], "onCarReset": [], # No handler
"onSentPing": [], # Only sync "onCarChanged": [], # No handler
"onChangePosition": [], # Only sync "onCarFocusMove": [], # No handler
"onPlayerDisconnect": [], "onSentPing": [], # Only sync, no handler
"onServerStopped": [], "onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler
} }
self.__async_events = { self.__async_events = {
"onServerStarted": [], "onServerStarted": [],
@@ -44,6 +46,8 @@ class EventsSystem:
"onCarDelete": [], "onCarDelete": [],
"onCarEdited": [], "onCarEdited": [],
"onCarReset": [], "onCarReset": [],
"onCarChanged": [],
"onCarFocusMove": [],
"onPlayerDisconnect": [], "onPlayerDisconnect": [],
"onServerStopped": [] "onServerStopped": []
} }
@@ -52,22 +56,28 @@ class EventsSystem:
"onInit": [], # onServerStarted "onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped "onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated "onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # TODO lua onPlayerConnecting "onPlayerConnecting": [], # No
"onPlayerJoining": [], # TODO lua onPlayerJoining "onPlayerJoining": [], # No
"onPlayerJoin": [], # onPlayerJoin "onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # TODO lua onPlayerDisconnect "onPlayerDisconnect": [], # onPlayerDisconnect
"onChatMessage": [], # onChatReceive "onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # "onCarSpawn "onVehicleSpawn": [], # onCarSpawn
"onVehicleEdited": [], # onCarEdited "onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete "onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset "onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged "onFileChanged": [], # TODO lua onFileChanged
"onConsoleInput": [], # kt.add_command
} }
def builtins_hook(self): def builtins_hook(self):
self.log.debug("used builtins_hook") self.log.debug("used builtins_hook")
builtins.ev = self builtins.ev = self
def is_event(self, event_name):
return (event_name in self.__async_events.keys() or
event_name in self.__events.keys() or
event_name in self.__lua_events.keys())
def register_event(self, event_name, event_func, async_event=False, lua=None): def register_event(self, event_name, event_func, async_event=False, lua=None):
self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', " self.log.debug(f"register_event(event_name='{event_name}', event_func='{event_func}', "
f"async_event={async_event}, lua_event={lua}):") f"async_event={async_event}, lua_event={lua}):")
@@ -80,9 +90,7 @@ class EventsSystem:
return return
if not callable(event_func): if not callable(event_func):
# TODO: i18n self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
self.log.error(f"Cannot add event '{event_name}'. "
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return return
if async_event or inspect.iscoroutinefunction(event_func): if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events: if event_name not in self.__async_events:
@@ -107,12 +115,10 @@ class EventsSystem:
data = await func(event_data) data = await func(event_data)
funcs_data.append(data) funcs_data.append(data)
except Exception as e: except Exception as e:
# TODO: i18n self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
self.log.exception(e) self.log.exception(e)
else: elif not self.is_event(event_name):
# TODO: i18n self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_event()?. Just skipping it...")
return funcs_data return funcs_data
@@ -127,12 +133,10 @@ class EventsSystem:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs} event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data)) funcs_data.append(func(event_data))
except Exception as e: except Exception as e:
# TODO: i18n self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.error(f'Error while calling "{event_name}"; In function: "{func.__name__}"')
self.log.exception(e) self.log.exception(e)
else: elif not self.is_event(event_name):
# TODO: i18n self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_async_event()?. Just skipping it...")
return funcs_data return funcs_data
@@ -146,21 +150,13 @@ class EventsSystem:
try: try:
func = lua.globals()[func_name] func = lua.globals()[func_name]
if not func: if not func:
self.log.warning(f"Cannot trigger local event: '{func_name}' not found!") self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
continue continue
fd = func(*args) fd = func(*args)
funcs_data.append(fd) funcs_data.append(fd)
except Exception as e: except Exception as e:
# TODO: i18n self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
self.log.error(f'Error: "{e}" - while calling lua event "{event_name}" with arguments: {args} - ' elif not self.is_event(event_name):
f'in function: "{func_name}"') self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
# self.log.exception(e)
else:
# TODO: i18n
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_lua_event() or MP.Trigger<>Event()?. "
f"Just skipping it...")
return funcs_data return funcs_data

View File

@@ -3,6 +3,7 @@ import json
import os import os
import platform import platform
import random import random
import re
import shutil import shutil
import threading import threading
import time import time
@@ -42,7 +43,7 @@ class EventTimer:
self.mp.TriggerLocalEvent(self.event_name) self.mp.TriggerLocalEvent(self.event_name)
# noinspection PyPep8Naming # noinspection PyPep8Naming,PyProtectedMember
class MP: class MP:
def __init__(self, name: str, lua: LuaRuntime): def __init__(self, name: str, lua: LuaRuntime):
@@ -63,6 +64,12 @@ class MP:
def _print(self, *args): def _print(self, *args):
args = list(args) args = list(args)
for i, arg in enumerate(args): for i, arg in enumerate(args):
if isinstance(arg, str):
try:
text = arg.encode("CP1251").decode(config.enc).replace("\u001b", "\x1b")
args[i] = re.sub(r'\x1b\[.*?m', '', text)
except UnicodeEncodeError:
pass
if "LuaTable" in str(type(arg)): if "LuaTable" in str(type(arg)):
args[i] = self._lua.globals().Util.JsonEncode(arg) args[i] = self._lua.globals().Util.JsonEncode(arg)
s = " ".join(map(str, args)) s = " ".join(map(str, args))
@@ -110,19 +117,15 @@ class MP:
try: try:
func = self._lua.globals()[func_name] func = self._lua.globals()[func_name]
if not func: if not func:
self.log.warning(f"Cannot trigger local event: '{func_name}' not found!") self.log.warning(i18n.events_lua_function_not_found.format(i18n.events_lua_local, func_name))
continue continue
fd = func(*args) fd = func(*args)
funcs_data.append(fd) funcs_data.append(fd)
except Exception as e: except Exception as e:
# TODO: i18n self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
self.log.error(f'Error: "{e}" - while calling lua event "{event_name}" with arguments: {args} - '
f'in function: "{func_name}"')
# self.log.exception(e)
else: else:
# TODO: i18n self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
self.log.warning(f"Event {event_name} does not exist, maybe ev.call_lua_event() or MP.Trigger<>Event()?. "
f"Just skipping it...")
return self._lua.table_from(funcs_data) return self._lua.table_from(funcs_data)
@@ -556,6 +559,7 @@ class FS:
return os.path.join(*args) return os.path.join(*args)
# noinspection PyProtectedMember
class LuaPluginsLoader: class LuaPluginsLoader:
def __init__(self, plugins_dir): def __init__(self, plugins_dir):
@@ -573,13 +577,10 @@ class LuaPluginsLoader:
def load(self): def load(self):
self.log.debug("Loading Lua plugins...") self.log.debug("Loading Lua plugins...")
# TODO: i18n self.log.info(i18n.plugins_lua_enabled)
self.log.info("You have enabled support for Lua plugins.") self.log.warning(i18n.plugins_lua_nuances_warning)
self.log.warning("There are some nuances to working with KuiToi. " self.log.warning(i18n.plugins_lua_legacy_config_create_warning)
"If you have a proposal for their solution, and it is related to KuiToi, " self.log.info(i18n.plugins_lua_legacy_config_create)
"please contact the developer.")
self.log.warning("Some BeamMP plugins require a correctly configured ServerConfig.toml file to function.")
self.log.info("Creating it.")
data = { data = {
"info": "ServerConfig.toml is created solely for backward compatibility support. " "info": "ServerConfig.toml is created solely for backward compatibility support. "
"This file will be updated every time the program is launched.", "This file will be updated every time the program is launched.",
@@ -667,6 +668,6 @@ class LuaPluginsLoader:
self.log.debug("Unloading lua plugins") self.log.debug("Unloading lua plugins")
for name, data in self.lua_plugins.items(): for name, data in self.lua_plugins.items():
if data['ok']: if data['ok']:
self.log.info(f"Unloading lua plugin: {name}") self.log.info(i18n.plugins_lua_unload.format(name))
for _, timer in data['lua'].globals().MP._event_timers.items(): for _, timer in data['lua'].globals().MP._event_timers.items():
timer.stop() timer.stop()

View File

@@ -91,6 +91,10 @@ class KuiToi:
return False return False
return bool(self.get_player(cid=pid, nick=nick)) return bool(self.get_player(cid=pid, nick=nick))
def add_command(self, key, func, man, desc, custom_completer) -> dict:
self.log.debug("Requests add_command")
return console.add_command(key, func, man, desc, custom_completer)
class PluginsLoader: class PluginsLoader:
@@ -127,21 +131,21 @@ class PluginsLoader:
try: try:
is_func = inspect.isfunction is_func = inspect.isfunction
if not is_func(plugin.load): if not is_func(plugin.load):
self.log.error('Function "def load():" not found.') self.log.error(i18n.plugins_not_found_load)
ok = False ok = False
if not is_func(plugin.start): if not is_func(plugin.start):
self.log.error('Function "def start():" not found.') self.log.error(i18n.plugins_not_found_start)
ok = False ok = False
if not is_func(plugin.unload): if not is_func(plugin.unload):
self.log.error('Function "def unload():" not found.') self.log.error(i18n.plugins_not_found_unload)
ok = False ok = False
if type(plugin.kt) != KuiToi: if type(plugin.kt) != KuiToi:
self.log.error(f'Attribute "kt" isn\'t KuiToi class. Plugin file: "{file_path}"') self.log.error(i18n.plugins_kt_invalid)
ok = False ok = False
except AttributeError: except AttributeError:
ok = False ok = False
if not ok: if not ok:
self.log.error(f'Plugin file: "{file_path}" is not a valid KuiToi plugin.') self.log.error(i18n.plugins_invalid.format(file_path))
return return
pl_name = plugin.kt.name pl_name = plugin.kt.name
@@ -181,9 +185,8 @@ class PluginsLoader:
self.loaded_str += f"{pl_name}:ok, " self.loaded_str += f"{pl_name}:ok, "
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}") self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
except Exception as e: except Exception as e:
# TODO: i18n
self.loaded_str += f"{file}:no, " self.loaded_str += f"{file}:no, "
self.log.error(f"Error while loading plugin: {file}; Error: {e}") self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
self.log.exception(e) self.log.exception(e)
async def start(self, _): async def start(self, _):

View File

@@ -1,48 +0,0 @@
{
"": "基础阶段",
"hello": "来自KuiToi服务器的问候",
"config_path": "使用{}进行配置。",
"init_ok": "初始化完成。",
"start": "服务器已启动!",
"stop": "服务器已停止!",
"": "服务器认证",
"auth_need_key": "需要BeamMP密钥才能启动",
"auth_empty_key": "BeamMP密钥为空",
"auth_cannot_open_browser": "无法打开浏览器:{}",
"auth_use_link": "使用此链接:{}",
"": "GUI阶段",
"GUI_yes": "是",
"GUI_no": "否",
"GUI_ok": "确定",
"GUI_cancel": "取消",
"GUI_need_key_message": "需要BeamMP密钥才能启动\n是否在浏览器中打开链接以获取密钥",
"GUI_enter_key_message": "请输入密钥:",
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}",
"": "Web阶段",
"web_start": "WebAPI已启动{}CTRL+C停止",
"": "命令man",
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法man COMMAND",
"help_message_man": "显示COMMAND的帮助页面。",
"man_for": "帮助页面",
"man_message_not_found": "man找不到帮助页面。",
"man_command_not_found": "man找不到\"{}\"命令!",
"": "命令help",
"man_message_help": "help - 显示命令的名称和简短描述。\n用法help [--raw]\n命令`help`列出所有可用的命令,并为每个命令提供简短描述。",
"help_message_help": "显示命令的名称和简短描述。",
"help_command": "命令",
"help_message": "文本",
"help_message_not_found": "无文本",
"": "命令stop",
"man_message_stop": "stop - 关闭服务器。\n用法stop",
"help_message_stop": "关闭服务器。",
"": "命令exit",
"man_message_exit": "exit - 关闭服务器。\n用法exit",
"help_message_exit": "关闭服务器。"
}

View File

@@ -1,48 +0,0 @@
{
"": "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": "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": "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 started at {} (Press CTRL+C to quit)",
"": "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 - 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 - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}

View File

@@ -1,48 +0,0 @@
{
"": "Basic phases",
"hello": "Привет из KuiToi-Server!",
"config_path": "Используй {} для настройки.",
"init_ok": "Инициализация окончена.",
"start": "Сервер запущен!",
"stop": "Сервер остановлен!",
"": "Server auth",
"auth_need_key": "Нужен BeamMP ключ для запуска!",
"auth_empty_key": "BeamMP ключ пустой!",
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
"auth_use_link": "Используй эту ссылку: {}",
"": "GUI phases",
"GUI_yes": "Да",
"GUI_no": "Нет",
"GUI_ok": "Окей",
"GUI_cancel": "Отмена",
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_enter_key_message": "Пожалуйста введите ключ:",
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
"": "Web phases",
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
"": "Command: man",
"man_message_man": "man - Показывает страничку помощи для COMMAND.\nИспользование: man COMMAND",
"help_message_man": "Показывает страничку помощи для COMMAND.",
"man_for": "Страничка помощи для",
"man_message_not_found": "man: Страничка помощи не найдена.",
"man_command_not_found": "man: Команда \"{}\" не найдена!",
"": "Command: help",
"man_message_help": "help - Показывает названия и краткое описание команд.\nИспользование: help [--raw]\nКоманда `help` выводит список всех доступных команд, и краткое описание для каждой команды.",
"help_message_help": "Показывает названия и краткое описание команд",
"help_command": "Команда",
"help_message": "Текст",
"help_message_not_found": "Нет текста",
"": "Command: stop",
"man_message_stop": "stop - Выключает сервер.\nИспользование: stop",
"help_message_stop": "Выключает сервер.",
"": "Command: exit",
"man_message_exit": "exit - Выключает сервер.\nИспользование: exit",
"help_message_exit": "Выключает сервер."
}

View File

@@ -1,93 +1,109 @@
class i18n: class i18n:
# Basic phases # Basic phases
hello: str = data["hello"] hello: str
config_path: str = data["config_path"] config_path: str
init_ok: str = data["init_ok"] init_ok: str
start: str = data["start"] start: str
stop: str = data["stop"] stop: str
# Server auth # Server auth
auth_need_key: str = data["auth_need_key"] auth_need_key: str
auth_empty_key: str = data["auth_empty_key"] auth_empty_key: str
auth_cannot_open_browser: str = data["auth_cannot_open_browser"] auth_cannot_open_browser: str
auth_use_link: str = data["auth_use_link"] auth_use_link: str
# GUI phases # GUI phases
GUI_yes: str = data["GUI_yes"] GUI_yes: str
GUI_no: str = data["GUI_no"] GUI_no: str
GUI_ok: str = data["GUI_ok"] GUI_ok: str
GUI_cancel: str = data["GUI_cancel"] GUI_cancel: str
GUI_need_key_message: str = data["GUI_need_key_message"] GUI_need_key_message: str
GUI_enter_key_message: str = data["GUI_enter_key_message"] GUI_enter_key_message: str
GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"] GUI_cannot_open_browser: str
# Web phases # Web phases
web_start: str = data["web_start"] web_start: str
# Core phrases
core_bind_failed: str
core_direct_mode: str
core_auth_server_error: str
core_auth_server_refused: str
core_auth_server_refused_no_reason: str
core_auth_server_refused_direct_node: str
core_auth_server_no_response: str
core_mods_loaded: str
core_identifying_connection: str
core_player_kick_outdated: str
core_player_kick_bad_key: str
core_player_kick_invalid_key: str
core_player_kick_auth_server_fail: str
core_player_kick_stale: str
core_player_kick_no_allowed_default_reason: str
core_player_kick_server_full: str
core_player_set_id: str
core_identifying_okay: str
# In-game phrases
game_welcome_message: str
# Client class phrases
client_mod_request: str
client_mod_sent: str
client_mod_sent_limit: str
client_mod_sent_error: str
client_sync_time: str
client_kicked: str
client_event_invalid_data: str
client_player_disconnected: str
# Events system
events_not_callable: str
events_not_found: str
events_calling_error: str
events_lua_function_not_found: str
events_lua_local: str
events_lua_calling_error: str
# Plugins loader
plugins_not_found_load: str
plugins_not_found_start: str
plugins_not_found_unload: str
plugins_kt_invalid: str
plugins_invalid: str
plugins_error_loading: str
# Lua plugins loader
plugins_lua_enabled: str
plugins_lua_nuances_warning: str
plugins_lua_legacy_config_create_warning: str
plugins_lua_legacy_config_create: str
plugins_lua_unload: str
# Command: man # Command: man
man_message_man: str = data["man_message_man"] man_message_man: str
help_message_man: str = data["help_message_man"] help_message_man: str
man_for: str = data["man_for"] man_for: str
man_message_not_found: str = data["man_message_not_found"] man_message_not_found: str
man_command_not_found: str = data["man_command_not_found"] man_command_not_found: str
# Command: help # Command: help
man_message_help: str = data["man_message_help"] man_message_help: str
help_message_help: str = data["help_message_help"] help_message_help: str
help_command: str = data["help_command"] help_command: str
help_message: str = data["help_message"] help_message: str
help_message_not_found: str = data["help_message_not_found"] help_message_not_found: str
# Command: stop # Command: stop
man_message_stop: str = data["man_message_stop"] man_message_stop: str
help_message_stop: str = data["help_message_stop"] help_message_stop: str
# Command: exit # Command: exit
man_message_exit: str = data["man_message_exit"] man_message_exit: str
help_message_exit: str = data["help_message_exit"] help_message_exit: str
data = {
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"start": "Server started!",
"stop": "Goodbye!",
"": "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: {}",
"": "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: {}",
"": "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: 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: stop",
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
"help_message_stop": "Server shutdown.",
"": "Command: exit",
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
"help_message_exit": "Server shutdown."
}

View File

@@ -8,136 +8,128 @@
# (c) kuitoi.su 2023 # (c) kuitoi.su 2023
import builtins import builtins
import json import json
import os
from json import JSONDecodeError from json import JSONDecodeError
from core.utils import get_logger from core.utils import get_logger
class i18n: class i18n:
data = {}
def __init__(self, data): def __init__(self, data):
# Basic phases i18n.data = data
self.hello: str = data["hello"]
self.config_path: str = data["config_path"]
self.init_ok: str = data["init_ok"]
self.start: str = data["start"]
self.stop: str = data["stop"]
# Server auth def __getattribute__(self, key):
self.auth_need_key: str = data["auth_need_key"] return i18n.data[key]
self.auth_empty_key: str = data["auth_empty_key"]
self.auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
self.auth_use_link: str = data["auth_use_link"]
# GUI phases
self.GUI_yes: str = data["GUI_yes"]
self.GUI_no: str = data["GUI_no"]
self.GUI_ok: str = data["GUI_ok"]
self.GUI_cancel: str = data["GUI_cancel"]
self.GUI_need_key_message: str = data["GUI_need_key_message"]
self.GUI_enter_key_message: str = data["GUI_enter_key_message"]
self.GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
# Web phases
self.web_start: str = data["web_start"]
# Command: man
self.man_message_man: str = data["man_message_man"]
self.help_message_man: str = data["help_message_man"]
self.man_for: str = data["man_for"]
self.man_message_not_found: str = data["man_message_not_found"]
self.man_command_not_found: str = data["man_command_not_found"]
# Command: help
self.man_message_help: str = data["man_message_help"]
self.help_message_help: str = data["help_message_help"]
self.help_command: str = data["help_command"]
self.help_message: str = data["help_message"]
self.help_message_not_found: str = data["help_message_not_found"]
# Command: help
self.man_message_stop: str = data["man_message_stop"]
self.help_message_stop: str = data["help_message_stop"]
# Command: exit
self.man_message_exit: str = data["man_message_exit"]
self.help_message_exit: str = data["help_message_exit"]
self.data = data
class MultiLanguage: class MultiLanguage:
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding=None): def __init__(self, language: str = None, files_dir="translates/", encoding=None):
if encoding is None: if encoding is None:
encoding = config.enc encoding = config.enc
if language is None: if language is None:
language = "en" language = "en"
self.__data = {} self.__data = {
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization completed.",
"start": "Server started!",
"stop": "Server stopped!",
"auth_need_key": "BeamMP key is required to run!",
"auth_empty_key": "BeamMP key is empty!",
"auth_cannot_open_browser": "Failed to open browser: {}",
"auth_use_link": "Use this link: {}",
"GUI_yes": "Yes",
"GUI_no": "No",
"GUI_ok": "OK",
"GUI_cancel": "Cancel",
"GUI_need_key_message": "BeamMP key is required to run!\nDo you want to open the link in your browser to get the key?",
"GUI_enter_key_message": "Please enter the key:",
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
"web_start": "WebAPI started on {} (CTRL+C to stop)",
"core_bind_failed": "Failed to bind port. Error: {}",
"core_direct_mode": "Server started in direct connection mode.",
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
"core_auth_server_no_response": "Failed to authenticate the server.",
"core_mods_loaded": "Loaded {} mods. {}Mb",
"core_identifying_connection": "Processing new connection...",
"core_player_kick_outdated": "Incorrect version of BeamMP.",
"core_player_kick_bad_key": "Invalid key passed!",
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
"core_player_kick_server_full": "Server is full.",
"core_player_set_id": "Player set ID {}",
"core_identifying_okay": "Successful login.",
"game_welcome_message": "Welcome {}!",
"client_mod_request": "Requested mod: {}",
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
"client_mod_sent_limit": " (limit {}Mb/s)",
"client_mod_sent_error": "Error sending mod: {}",
"client_sync_time": "Sync time {}sec.",
"client_kicked": "Kicked for reason: \"{}\"",
"client_event_invalid_data": "Invalid data returned from event: {}",
"client_player_disconnected": "Left the server. Playtime: {} min",
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
"events_lua_local": "local ",
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
"plugins_not_found_load": "Function \"def load():\" not found.",
"plugins_not_found_start": "Function \"def start():\" not found.",
"plugins_not_found_unload": "Function \"def unload():\" not found.",
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
"plugins_lua_enabled": "You have enabled Lua plugin support.",
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
"plugins_lua_legacy_config_create": "Creating it.",
"plugins_lua_unload": "Stopping Lua plugin: {}",
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Shows the 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!",
"man_message_help": "help - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
"help_message_help": "Shows the names and brief descriptions of commands",
"help_command": "Command",
"help_message": "Text",
"help_message_not_found": "No text found",
"man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}
self.__en_data = self.__data.copy()
self.__i18n = None self.__i18n = None
self.__encoding = encoding self.__encoding = encoding
self.language = language self.language = language
if not os.path.exists(files_dir):
os.makedirs(files_dir)
if not os.path.exists(files_dir + "en.json"):
with open(files_dir + "en.json", "w") as f:
f.write(json.dumps(self.__en_data, indent=2))
self.files_dir = files_dir self.files_dir = files_dir
self.log = get_logger("i18n") self.log = get_logger("i18n")
self.fi = False
self.set_language(language) self.set_language(language)
def set_language(self, language): def set_language(self, language="en"):
if language is None: if self.language == language and self.fi:
language = "en" return
self.log.debug(f"set_language({language})")
self.language = language
if language != "en":
self.open_file()
else: else:
# noinspection PyDictDuplicateKeys self.fi = True
self.__data = { self.log.debug(f"set_language({language})")
"": "Basic phases", self.language = language
"hello": "Greetings from KuiToi Server!", self.open_file()
"config_path": "Use {} to configure.",
"init_ok": "Initialization complete.",
"start": "Server started!",
"stop": "Server stopped!",
"": "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": "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 started at {} (Press CTRL+C to quit)",
"": "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 - 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 - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}
self.__i18n = i18n(self.__data) self.__i18n = i18n(self.__data)
def open_file(self): def open_file(self):
@@ -149,9 +141,9 @@ class MultiLanguage:
return return
except JSONDecodeError: except JSONDecodeError:
self.log.error( self.log.error(
f"Localisation \"{self.language}.json\" have JsonDecodeError. Using default localisation: en.") f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.")
except FileNotFoundError: except FileNotFoundError:
self.log.warning(f"Localisation \"{self.language}.json\" not found; Using default localisation: en.") self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.")
self.set_language("en") self.set_language("en")
def builtins_hook(self) -> None: def builtins_hook(self) -> None:

107
src/translates/cn.json Normal file
View File

@@ -0,0 +1,107 @@
{
"": "基本阶段",
"hello": "来自KuiToi-Server的问候",
"config_path": "使用{}进行配置。",
"init_ok": "初始化完成。",
"start": "服务器已启动!",
"stop": "服务器已停止!",
"": "服务器认证",
"auth_need_key": "需要BeamMP密钥才能运行",
"auth_empty_key": "BeamMP密钥为空",
"auth_cannot_open_browser": "无法打开浏览器:{}",
"auth_use_link": "使用此链接:{}",
"": "GUI阶段",
"GUI_yes": "是",
"GUI_no": "否",
"GUI_ok": "确定",
"GUI_cancel": "取消",
"GUI_need_key_message": "需要BeamMP密钥才能运行\n您是否要在浏览器中打开链接获取密钥",
"GUI_enter_key_message": "请输入密钥:",
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接{}",
"": "Web阶段",
"web_start": "WebAPI已启动于{}按CTRL+C停止",
"": "核心短语",
"core_bind_failed": "无法绑定端口。错误:{}",
"core_direct_mode": "服务器以直接连接模式启动。",
"core_auth_server_error": "从BeamMP认证服务器接收到无效响应。",
"core_auth_server_refused": "BeamMP认证服务器拒绝了您的密钥。原因{}",
"core_auth_server_refused_no_reason": "BeamMP认证服务器没有提供原因。",
"core_auth_server_refused_direct_node": "服务器仍在运行,但以直接连接模式运行。",
"core_auth_server_no_response": "无法验证服务器。",
"core_mods_loaded": "已加载{}个模组。{}Mb",
"core_identifying_connection": "正在处理新连接...",
"core_player_kick_outdated": "BeamMP版本不正确。",
"core_player_kick_bad_key": "传递的密钥无效!",
"core_player_kick_invalid_key": "无效的密钥!请重新启动游戏。",
"core_player_kick_auth_server_fail": "BeamMP认证服务器失败请在5分钟后再次尝试连接。",
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
"core_player_kick_server_full": "服务器已满。",
"core_player_set_id": "玩家设置ID {}",
"core_identifying_okay": "成功登录。",
"": "游戏内短语",
"game_welcome_message": "欢迎{}",
"": "客户端类短语",
"client_mod_request": "请求模组:{}",
"client_mod_sent": "已发送模组:大小:{}mb速度{}Mb/s{}秒)",
"client_mod_sent_limit": "(限制{}Mb/s",
"client_mod_sent_error": "发送模组时出错:{}",
"client_sync_time": "同步时间{}秒。",
"client_kicked": "因\"{}\"原因被踢出。",
"client_event_invalid_data": "从事件返回的数据无效:{}",
"client_player_disconnected": "离开服务器。游戏时间:{}分钟。",
"": "事件系统",
"events_not_callable": "无法添加事件\"{}\"。请改用\"{}\"。跳过...",
"events_not_found": "事件\"{}\"未注册。也许{}?跳过...",
"events_calling_error": "调用函数\"{}\"时出错。",
"events_lua_function_not_found": "无法调用{}lua事件 - 未找到\"{}\"。",
"events_lua_local": "本地 ",
"events_lua_calling_error": "错误:\"{}\" - 调用lua事件\"{}\"时出错,函数:\"{}\",参数:{}",
"": "插件加载器",
"plugins_not_found_load": "未找到\"def load():\"函数。",
"plugins_not_found_start": "未找到\"def start():\"函数。",
"plugins_not_found_unload": "未找到\"def unload():\"函数。",
"plugins_kt_invalid": "“kt”变量不属于KuiToi类。",
"plugins_invalid": "无法在KuiToi中运行插件\"{}\"。",
"plugins_error_loading": "加载插件{}时出错:{}",
"": "Lua插件加载器",
"plugins_lua_enabled": "您已启用Lua插件支持。",
"plugins_lua_nuances_warning": "在使用KuiToi时有一些细微差别。如果您有关于解决方案的建议并且它与KuiToi相关请联系开发人员。",
"plugins_lua_legacy_config_create_warning": "一些BeamMP插件需要一个正确配置的ServerConfig.toml文件才能正常运行。",
"plugins_lua_legacy_config_create": "正在创建。",
"plugins_lua_unload": "停止Lua插件{}",
"": "命令man",
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法man COMMAND",
"help_message_man": "显示COMMAND的帮助页面。",
"man_for": "帮助页面",
"man_message_not_found": "man找不到帮助页面。",
"man_command_not_found": "man找不到命令\"{}\"",
"": "命令help",
"man_message_help": "help - 显示命令的名称和简要说明。\n用法help [--raw]\n`help`命令显示所有可用命令的名称和简要说明。",
"help_message_help": "显示命令的名称和简要说明。",
"help_command": "命令",
"help_message": "文本",
"help_message_not_found": "未找到文本。",
"": "命令stop",
"man_message_stop": "stop - 停止服务器。\n用法stop",
"help_message_stop": "停止服务器。",
"": "命令exit",
"man_message_exit": "exit - 停止服务器。\n用法exit",
"help_message_exit": "停止服务器。"
}

107
src/translates/en.json Normal file
View File

@@ -0,0 +1,107 @@
{
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} to configure.",
"init_ok": "Initialization completed.",
"start": "Server started!",
"stop": "Server stopped!",
"": "Server auth",
"auth_need_key": "BeamMP key is required to run!",
"auth_empty_key": "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": "BeamMP key is required to run!\nDo you want to open the link in your browser to get 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 started on {} (CTRL+C to stop)",
"": "Core phrases",
"core_bind_failed": "Failed to bind port. Error: {}",
"core_direct_mode": "Server started in direct connection mode.",
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
"core_auth_server_no_response": "Failed to authenticate the server.",
"core_mods_loaded": "Loaded {} mods. {}Mb",
"core_identifying_connection": "Processing new connection...",
"core_player_kick_outdated": "Incorrect version of BeamMP.",
"core_player_kick_bad_key": "Invalid key passed!",
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
"core_player_kick_server_full": "Server is full.",
"core_player_set_id": "Player set ID {}",
"core_identifying_okay": "Successful login.",
"": "In-game phrases",
"game_welcome_message": "Welcome {}!",
"": "Client class phrases",
"client_mod_request": "Requested mod: {}",
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
"client_mod_sent_limit": " (limit {}Mb/s)",
"client_mod_sent_error": "Error sending mod: {}",
"client_sync_time": "Sync time {}sec.",
"client_kicked": "Kicked for reason: \"{}\"",
"client_event_invalid_data": "Invalid data returned from event: {}",
"client_player_disconnected": "Left the server. Playtime: {} min",
"": "Events system",
"events_not_callable": "Unable to add event \"{}\". Use \"{}\" instead. Skipping...",
"events_not_found": "Event \"{}\" is not registered. Maybe {}? Skipping...",
"events_calling_error": "Error calling \"{}\" in function \"{}\".",
"events_lua_function_not_found": "Unable to call {}lua event - \"{}\" not found.",
"events_lua_local": "local ",
"events_lua_calling_error": "Error: \"{}\" - calling lua event \"{}\", function: \"{}\", arguments: {}",
"": "Plugins loader",
"plugins_not_found_load": "Function \"def load():\" not found.",
"plugins_not_found_start": "Function \"def start():\" not found.",
"plugins_not_found_unload": "Function \"def unload():\" not found.",
"plugins_kt_invalid": "\"kt\" variable does not belong to the KuiToi class.",
"plugins_invalid": "Plugin \"{}\" cannot be run in KuiToi.",
"plugins_error_loading": "An error occurred while loading the plugin {}: {}",
"": "Lua plugins loader",
"plugins_lua_enabled": "You have enabled Lua plugin support.",
"plugins_lua_nuances_warning": "There are some nuances when working with Kuiti. If you have a suggestion for their solution, and it is related to KuiToi, please contact the developer.",
"plugins_lua_legacy_config_create_warning": "Some BeamMP plugins require a properly configured ServerConfig.toml file to function.",
"plugins_lua_legacy_config_create": "Creating it.",
"plugins_lua_unload": "Stopping Lua plugin: {}",
"": "Command: man",
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Shows the 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 - Shows the names and brief descriptions of commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands, with a brief description for each command.",
"help_message_help": "Shows the names and brief descriptions of commands",
"help_command": "Command",
"help_message": "Text",
"help_message_not_found": "No text found",
"": "Command: stop",
"man_message_stop": "stop - Stops the server.\nUsage: stop",
"help_message_stop": "Stops the server.",
"": "Command: exit",
"man_message_exit": "exit - Stops the server.\nUsage: exit",
"help_message_exit": "Stops the server."
}

107
src/translates/ru.json Normal file
View File

@@ -0,0 +1,107 @@
{
"": "Basic phases",
"hello": "Привет из KuiToi-Server!",
"config_path": "Используй {} для настройки.",
"init_ok": "Инициализация окончена.",
"start": "Сервер запущен!",
"stop": "Сервер остановлен!",
"": "Server auth",
"auth_need_key": "Нужен BeamMP ключ для запуска!",
"auth_empty_key": "BeamMP ключ пустой!",
"auth_cannot_open_browser": "Не получилось открыть браузер: {}",
"auth_use_link": "Используй эту ссылку: {}",
"": "GUI phases",
"GUI_yes": "Да",
"GUI_no": "Нет",
"GUI_ok": "Окей",
"GUI_cancel": "Отмена",
"GUI_need_key_message": "Нужен BeamMP ключ для запуска!\nХотите открыть ссылку в браузере для получения ключа?",
"GUI_enter_key_message": "Пожалуйста введите ключ:",
"GUI_cannot_open_browser": "Не получилось открыть браузер.\nИспользуй эту ссылку: {}",
"": "Web phases",
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
"": "Core phrases",
"core_bind_failed": "Не получилось занять порт. Ошибка: {}",
"core_direct_mode": "Сервер запушен в режиме прямого подключения.",
"core_auth_server_error": "Поступил не корректный ответ от сервером авторизации BeamMP.",
"core_auth_server_refused": "Сервер авторизации BeamMP отклонил ваш ключ. Причина: {}",
"core_auth_server_refused_no_reason": "Сервер авторизации BeamMP не сообщил причины.",
"core_auth_server_refused_direct_node": "Сервер всё ещё работает, но в режиме прямого подключения.",
"core_auth_server_no_response": "Не получилось авторизовать сервер.",
"core_mods_loaded": "Загружено {} модов. {}Мб",
"core_identifying_connection": "Обработка нового подключения...",
"core_player_kick_outdated": "Не подходящая версия BeamMP.",
"core_player_kick_bad_key": "Передан не правильный ключ!",
"core_player_kick_invalid_key": "Неверный ключ! Пожалуйста, перезапустите свою игру.",
"core_player_kick_auth_server_fail": "Сбой сервера аутентификации! Попробуйте снова подключиться через 5 минут.",
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
"core_player_kick_server_full": "Сервер полон.",
"core_player_set_id": "Игрок получил ID {}",
"core_identifying_okay": "Успешный вход.",
"": "In-game phrases",
"game_welcome_message": "Добро пожаловать {}!",
"": "Client class phrases",
"client_mod_request": "Запрошен мод: {}",
"client_mod_sent": "Мод отправлен: Вес: {}мб, Скорость: {}Мб/с ({}сек)",
"client_mod_sent_limit": " (лимит {}Мб/с)",
"client_mod_sent_error": "Ошибка при отправке мода: {}",
"client_sync_time": "Время синхронизации {}сек.",
"client_kicked": "Кикнут по причине: \"{}\"",
"client_event_invalid_data": "Из ивента вернулись не верные данные: {}",
"client_player_disconnected": "Вышел с сервера. Время игры: {} мин",
"": "Events system",
"events_not_callable": "Невозможно добавить ивент \"{}\". Использую лучше \"{}\". Скип...",
"events_not_found": "Ивент \"{}\" не зарегистрирован. Может {}? Скип...",
"events_calling_error": "Ошибка во время вызова \"{}\" в функции \"{}\".",
"events_lua_function_not_found": "Невозможно вызвать {}lua ивент - \"{}\" не найдена.",
"events_lua_local": "локальный ",
"events_lua_calling_error": "Ошибка: \"{}\" - во время вызова lua ивента \"{}\", функция: \"{}\" , аргументы: {}",
"": "Plugins loader",
"plugins_not_found_load": "Функция \"def load():\" не найдена.",
"plugins_not_found_start": "Функция \"def start():\" не найдена.",
"plugins_not_found_unload": "Функция \"def unload():\" не найдена.",
"plugins_kt_invalid": "Переменная \"kt\" не принадлежит классу KuiToi.",
"plugins_invalid": "Плагин: \"{}\" - не может быть запущен в KuiToi.",
"plugins_error_loading": "Произошла ошибка при загрузке плагина {}: {}",
"": "Lua plugins loader",
"plugins_lua_enabled": "Вы включили поддержку плагинов Lua.",
"plugins_lua_nuances_warning": "В работе с Kuiti есть некоторые нюансы. Если у вас есть предложение по их решению, и оно связано с KuiToi, пожалуйста, свяжитесь с разработчиком.",
"plugins_lua_legacy_config_create_warning": "Для работы некоторых плагинов BeamMP требуется правильно настроенный файл ServerConfig.toml.",
"plugins_lua_legacy_config_create": "Создаю его.",
"plugins_lua_unload": "Останавливаю Lua плагин: {}",
"": "Command: man",
"man_message_man": "man - Показывает страничку помощи для COMMAND.\nИспользование: man COMMAND",
"help_message_man": "Показывает страничку помощи для COMMAND.",
"man_for": "Страничка помощи для",
"man_message_not_found": "man: Страничка помощи не найдена.",
"man_command_not_found": "man: Команда \"{}\" не найдена!",
"": "Command: help",
"man_message_help": "help - Показывает названия и краткое описание команд.\nИспользование: help [--raw]\nКоманда `help` выводит список всех доступных команд, и краткое описание для каждой команды.",
"help_message_help": "Показывает названия и краткое описание команд",
"help_command": "Команда",
"help_message": "Текст",
"help_message_not_found": "Нет текста",
"": "Command: stop",
"man_message_stop": "stop - Выключает сервер.\nИспользование: stop",
"help_message_stop": "Выключает сервер.",
"": "Command: exit",
"man_message_exit": "exit - Выключает сервер.\nИспользование: exit",
"help_message_exit": "Выключает сервер."
}

BIN
win-logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

19
win-metadata.yml Normal file
View File

@@ -0,0 +1,19 @@
# pip install pyinstaller-versionfile
# create-version-file win-metadata.yml --outfile win-ver_info.txt
Version: 0.4.5
CompanyName: KuiToi
FileDescription: KuiToi Server
InternalName: KuiToi Server
LegalCopyright: © Maxim Khomutov
OriginalFilename: KuiToi-Server.exe
ProductName: KuiToi Server
Translation:
# ru-RU
- langID: 1049
charsetID: 1251
# en-US
- langID: 1033
charsetID: 1251
# zh-CN
- langID: 2052
charsetID: 950

85
win-pyinstaller.json Normal file
View File

@@ -0,0 +1,85 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/src/main.py"
},
{
"optionDest": "onefile",
"value": true
},
{
"optionDest": "console",
"value": true
},
{
"optionDest": "icon_file",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/win-logo.ico"
},
{
"optionDest": "name",
"value": "KuiToi-Server"
},
{
"optionDest": "ascii",
"value": true
},
{
"optionDest": "clean_build",
"value": false
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "version_file",
"value": "C:/Users/Santa/PycharmProjects/kuitoi-Server/win-ver_info.txt"
},
{
"optionDest": "embed_manifest",
"value": true
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "win_private_assemblies",
"value": false
},
{
"optionDest": "win_no_prefer_redirects",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}