246 Commits

Author SHA1 Message Date
b4b3953194 [!] FIX i18n 2024-07-16 18:32:08 +03:00
b73674f409 [+] requests 2024-07-16 18:08:54 +03:00
5330943d6f [>] snowman > unicycle 2024-07-16 18:08:39 +03:00
c533702cdc [+] Now localizations download from github 2024-07-16 17:49:21 +03:00
3b9cea5cae [-] core_player_set_id 2024-07-16 17:21:17 +03:00
a923dbea1f [~] 0.4.5 > 0.4.6
[+] Recreate config (if empty)
[+] Hidden config
[!] FIX rl
[!] Update getting loop
[-] core_player_set_id
[~] Update copyrights
[~] Minor
2024-07-16 17:18:49 +03:00
2eb7e8801b [+] rl.parse_console 2024-07-15 17:41:14 +03:00
c4a34c6630 [~] Minor
[>] main files > init.py
[+] RateLimiter
[~] Update BeamMP_version
2024-07-15 16:32:22 +03:00
8fbd2cc330 [~] Update versions in req.txt
[~] Rename register_event > register
2024-07-15 11:39:31 +03:00
c838d42dd8 [+] Basic events
[~] Remove double texts
2024-06-28 17:59:29 +03:00
274bdead2f [>] TODOs to md
[>] windows's blobs to win/
2024-06-28 17:51:12 +03:00
163b233306 Fix lua client handler #1 2023-08-17 23:54:07 +03:00
c6fba31c89 minor 2023-08-17 23:48:56 +03:00
59c113ade5 minor 2023-08-17 21:25:52 +03:00
a06c48dca5 Add _synced #1 2023-08-17 21:25:47 +03:00
42e4a8e05e Feature ev_data is None -> continue 2023-08-17 18:32:48 +03:00
074589da47 FIX GetPlayers #1 2023-08-17 18:27:16 +03:00
50c12f4b5c FIX _identifiers and e | e 2023-08-17 18:26:53 +03:00
f145048cc1 FIX #1 2023-08-16 05:11:48 +03:00
68bf7d0d00 More debug info 2023-08-16 05:02:51 +03:00
e9815cdfcf FIX _get_player #1 2023-08-16 05:02:39 +03:00
acb2b45626 linux fix 2023-08-16 01:03:02 +03:00
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
4276eb0fdb Use encoding="CP1251" for lua plugins; 2023-07-24 04:58:08 +03:00
d92aae7c47 I hate cobalt: cobaltSysChar!!!!!!!!! 2023-07-24 04:54:55 +03:00
6ccbe99d47 Pretty errors 2023-07-24 04:54:36 +03:00
a50decb470 Lua... 2023-07-24 01:30:11 +03:00
9079750576 __gracefully_kick 2023-07-24 00:03:04 +03:00
e440cdf022 Minor updates;
Memory fix.. :)
UDP Fix;
2023-07-24 00:02:23 +03:00
43fd56f327 Minor updates.. 2023-07-23 23:06:29 +03:00
2368fec501 Update TODOs 2023-07-23 04:29:52 +03:00
ecb9886882 Fix focus_car 2023-07-23 02:37:15 +03:00
9a8bcbfae0 Add player.focus_car 2023-07-23 01:57:01 +03:00
ddcfa56467 cn.json 2023-07-23 00:26:07 +03:00
9e1017609c Add cn translations 2023-07-23 00:25:42 +03:00
d76262fc43 Fix 2023-07-23 00:17:32 +03:00
9253f24421 Update setup page 2023-07-23 00:10:07 +03:00
8435b00617 Add cn translation 2023-07-23 00:01:33 +03:00
847fe68417 Update en translation 2023-07-22 23:59:21 +03:00
5f32bf8423 Update TODOs 2023-07-22 23:31:24 +03:00
139143c517 Add логику 2023-07-22 23:28:41 +03:00
4bd2e28a21 md... 2023-07-22 23:28:05 +03:00
28203c5836 Заглушка 2023-07-22 23:26:58 +03:00
7de5837db4 Player class 2023-07-22 23:26:47 +03:00
cb00829ae7 client.cars -> client._cars;
Add some handlers;
2023-07-22 23:26:30 +03:00
59b0e58801 links fix 2023-07-22 23:14:47 +03:00
5b814efbb9 Minor update 2023-07-22 21:34:52 +03:00
17b8be1b9d Update KuiToi Class docs 2023-07-22 21:33:46 +03:00
25ac16c300 Add new Methods 2023-07-22 21:33:33 +03:00
1fac1d5ae9 Minor get_client update 2023-07-22 21:20:44 +03:00
d6021ddc2d Add pid (PlayerId) 2023-07-22 21:13:57 +03:00
8e8c66c3bf Optimize imports 2023-07-22 20:58:26 +03:00
4273571d97 call_lua_event 2023-07-22 20:45:36 +03:00
71df291391 Add KuiTio docs 2023-07-22 20:45:22 +03:00
cf0f397465 Move to examples 2023-07-22 18:15:54 +03:00
7b579d2916 Lua example 2023-07-22 18:14:57 +03:00
a3386339d0 Ньансы работы Lua 2023-07-22 17:59:29 +03:00
bf0a3f3feb Update TODOs; 2023-07-22 07:56:55 +03:00
5e071c5705 EventTimer Ready;
Unload lua plugins;
2023-07-22 07:56:23 +03:00
132beb0dd6 Add MP.CreateTimer(), MP.CreateEventTimer(), MP.CancelEventTimer() 2023-07-22 07:48:35 +03:00
33f2d2ba72 Add call_lua_events() 2023-07-22 06:28:08 +03:00
90113179d7 Add ServerConfig.toml generation; 2023-07-22 06:00:48 +03:00
cd178b815a Add log_chat; 2023-07-22 05:49:52 +03:00
c1f3983856 Minor 2023-07-22 05:23:56 +03:00
96cc4b08db Add warnings to lua;
Add MP.Settings;
2023-07-22 05:23:02 +03:00
d13a319f39 Remove LuaPluginsLoader from imports;
Add use_lua to config;
2023-07-22 05:05:15 +03:00
f24ae23eac Some fixes; 2023-07-22 05:01:11 +03:00
b31b01d137 Update build 2023-07-22 05:00:44 +03:00
e7be3c88be Configs 2023-07-22 04:44:23 +03:00
2dd8b5f5eb _event_waiters;
Recreate loading lua plugins;
2023-07-22 04:16:16 +03:00
84c45d321a Minor update; 2023-07-22 03:51:58 +03:00
b1162af681 Update TODOs 2023-07-22 02:53:28 +03:00
91c9cd8454 CreateTimer 2023-07-22 02:52:57 +03:00
b8326ecdf8 Refactored for pretty view error; 2023-07-22 02:49:13 +03:00
c068629c83 Add calling lua events 2023-07-22 02:48:42 +03:00
905c0a361d Add GetPlayerIDByName (Non docs...); 2023-07-22 02:48:23 +03:00
d7073d9124 New print (Now print tables as json);
Fix _recursive_dict_encode;
Fix lua.globals().onInit();
2023-07-22 01:48:55 +03:00
1b5ddbdd45 Refactor package.path;
Fix: Now .lua run from loadfrom(file);
2023-07-22 00:20:24 +03:00
0d3699bfee Fix lpl.load(); 2023-07-21 22:55:56 +03:00
b3dffe74ec Lau plugins now run in thread;
Remove unused event;
Add event from to Lua;
2023-07-21 22:51:22 +03:00
2992c9cbab Update TODOs 2023-07-21 22:34:06 +03:00
43518ac57c Update TODOs 2023-07-21 22:33:57 +03:00
a96e8111e3 Add TriggerClientEventJson,JsonDecode, JsonPrettify, JsonMinify, JsonFlatten, JsonUnflatten, JsonDiff; 2023-07-21 22:33:44 +03:00
ecf06bf1c9 Fix Util.JsonEncode 2023-07-21 20:11:46 +03:00
8d57db4a23 Add Util.JsonEncode 2023-07-21 20:06:38 +03:00
3d9e08d05d Util.Random* 2023-07-21 18:37:42 +03:00
ac5f5ee894 Update TODOs 2023-07-21 18:28:27 +03:00
92880a94df TriggerClientEvent 2023-07-21 18:28:00 +03:00
dcc1f14b17 Update TODOs 2023-07-21 17:42:19 +03:00
f9f4df7438 Add TriggerLocalEvent, TriggerGlobalEvent 2023-07-21 17:42:00 +03:00
4f7e83a00f TODO CreateEventTimer, CreateEventTimer 2023-07-21 15:53:47 +03:00
aa6716fa75 Update TODOs 2023-07-21 15:53:30 +03:00
6fa07f3e07 Add FP class;
Add Util class (WIP);
2023-07-21 15:49:22 +03:00
9ae200d48a Fix reg and add call_lua_event();
Add call_lua_event("onChatMessage");
New MP.RegisterEvent();
Add GetPositionRaw();
2023-07-21 05:22:32 +03:00
3b5324d115 __gracefully_kick;
_last_position;
car position - client.cars[car_id]['pos'];
2023-07-21 04:55:25 +03:00
f181a82e0e Update warnings 2023-07-21 02:45:09 +03:00
b271c80e39 Update TODOs 2023-07-21 02:42:56 +03:00
ef9a55c407 MP Class +- ready.. 2023-07-21 02:42:04 +03:00
98b4878339 Add client.identifiers; 2023-07-21 02:41:45 +03:00
b345588c02 Prepare for lua events;
Add _get_player for info;
client.send_message();
2023-07-21 01:15:31 +03:00
c8fea133ba Add Lua API: GetOSName, GetServerVersion 2023-07-20 15:15:26 +03:00
b80d519c8d Update TODOs 2023-07-20 06:48:08 +03:00
2ace3fcd17 Update TODOs 2023-07-20 06:47:26 +03:00
dbe8b14d7f Add lua loading; Not supporting for now... 2023-07-20 06:44:23 +03:00
752e981462 Is this LUAAAAA? No... 2023-07-20 05:21:00 +03:00
744a7347a3 Prepare for lua support;
Minor update;
2023-07-20 04:59:12 +03:00
8139cbf8bc 0.4.0 -> 0.4.1 2023-07-20 04:10:51 +03:00
eec7c8129d Add snowman support (Only one per player);
Optimize getting car_id;
Minor fixes;
2023-07-20 04:07:39 +03:00
158599dfc5 Debug t and m codes 2023-07-20 02:22:20 +03:00
06bd50f0fa Add UDP events: onChangePosition, onSentPing 2023-07-20 01:59:30 +03:00
e086fea2e9 Update __build__; 2023-07-20 01:56:36 +03:00
b6038ee6d0 Move code from case to func;
Add events for cars: onCarDelete, onCarReset;
Minor updates;
2023-07-20 01:54:52 +03:00
147e76e089 Remove some debug info;
Optimize code.
2023-07-20 01:38:56 +03:00
56b9049dcb BeamMP_version 2023-07-20 01:37:52 +03:00
78d323644d Remove some debug data! 2023-07-20 01:34:39 +03:00
310c47162c FIX config.enc 2023-07-20 01:27:01 +03:00
27d49cf5cc Update TODOs 2023-07-20 01:15:32 +03:00
a5a7a5dfc9 Add events onCarSpawn, onCarEdited 2023-07-19 22:59:30 +03:00
f6ff018b03 connect_time 2023-07-19 21:53:08 +03:00
1829113ae5 Add upload use_queue 2023-07-19 21:22:29 +03:00
e72c371e20 Minor updates 2023-07-19 21:10:26 +03:00
57b7cebeca Minor updates 2023-07-19 21:10:04 +03:00
2a2d55946e Update config_provider 2023-07-19 21:04:05 +03:00
ea2d715cae DO Speed limiter;
Minor fixes;
2023-07-19 20:23:16 +03:00
102891c8e8 0.3.0 -> 0.4.0 2023-07-19 03:53:27 +03:00
46b0419340 Update TODOs 2023-07-19 03:50:33 +03:00
47cca3a0d8 Add UDP send;
Remove rights to spawn unicycle (For now);
Minor updates;
2023-07-19 03:48:57 +03:00
77ee76c0c0 UDP Part ready! 2023-07-19 03:45:02 +03:00
852e977a75 Fix set udp sock 2023-07-18 22:59:12 +03:00
407127ec97 Typing update 2023-07-18 22:58:34 +03:00
7dd3faac12 Update TODOs 2023-07-18 22:43:49 +03:00
ef69df10d6 Compress DATA 2023-07-18 22:40:35 +03:00
a226b17612 Compress DATA 2023-07-18 22:40:27 +03:00
69348e9339 Some fixes in onChatReceive 2023-07-18 22:33:49 +03:00
31d8cf7842 try to check_alive 2023-07-18 22:33:15 +03:00
45d45a820c Update TODOs 2023-07-18 22:31:41 +03:00
aa440a1e3d Update TODOs 2023-07-18 22:31:14 +03:00
63c9515e86 Add UDP part 2023-07-18 22:08:00 +03:00
cfeb2e9823 heartbeat fix 2023-07-18 19:53:02 +03:00
85b85114b5 heartbeat fix 2023-07-18 19:52:44 +03:00
792884d7b0 Disable __handle_packet...;
Fix "Invalid packet - header negative";
Fix _get_cid_vid;
Add dta to _handle_vehicle_codes;
Fix disconnected message.
2023-07-18 05:35:34 +03:00
a5b087f8b4 Disable __handle_packet...;
Fix "Invalid packet - header negative";
Fix _get_cid_vid;
Add dta to _handle_vehicle_codes;
Fix disconnected message.
2023-07-18 05:35:17 +03:00
a01567c89a Auto removing cars after disconnect 2023-07-18 04:14:46 +03:00
041883644c UDP... 2023-07-18 04:10:56 +03:00
3d33eec5fd Minor fix 2023-07-18 04:10:42 +03:00
3f2c5b24f9 Add Disconnected message 2023-07-17 22:26:42 +03:00
b7ea7ff362 Beta!!! 2023-07-17 21:28:25 +03:00
07ec15170b 0.2.3 -> 0.3.0 2023-07-17 21:26:28 +03:00
eb88af247c Add reset and edit for cars 2023-07-17 21:23:40 +03:00
6dedf518e2 Update TODOs 2023-07-17 21:22:58 +03:00
69ee180128 Fix 2023-07-17 19:42:56 +03:00
98f86b2248 Minor update 2023-07-17 18:16:07 +03:00
98ef332193 Update TODOs 2023-07-17 17:11:59 +03:00
642c91d59c Packets handled (Recursive finding second packet) 2023-07-17 17:04:25 +03:00
acdb32d900 Update TODOs 2023-07-17 14:25:07 +03:00
50b1e7b176 Recreate cars system;
Move code "O" to client._handle_vehicle_codes();
2023-07-17 14:18:52 +03:00
c9e6a0a9cd Change evens naming semantic 2023-07-16 16:14:16 +03:00
cd098571d9 Change evens naming semantic 2023-07-16 16:14:00 +03:00
a73b14f9b4 Fix car spawning 2023-07-16 10:42:34 +03:00
e3e5c6ecbb Update TODOs 2023-07-16 09:50:02 +03:00
5953923368 DO Sync cars;
DO Create cars;
2023-07-16 09:48:53 +03:00
580b836e39 Minor fixes 2023-07-16 09:37:38 +03:00
4974d48411 0.2.2 -> 0.2.3 2023-07-16 03:07:47 +03:00
3b7842aa50 Update modules version 2023-07-16 03:06:38 +03:00
db6eb361b8 0.2.2 -> 0.2.3 2023-07-16 03:06:22 +03:00
479525a66e Remove nick, roles from protected;
Change _tcp_send -> _send;
Now _send data may be str;
Rewrite to_all handler;
Add chat handler;
2023-07-16 02:55:09 +03:00
6d4bc1e72c Minor update 2023-07-16 02:52:20 +03:00
9b3677de46 Minor update 2023-07-16 02:52:15 +03:00
58137752c5 Add commands plugins, pl 2023-07-16 02:50:25 +03:00
220c6068e4 Minor update 2023-07-16 02:49:58 +03:00
a9dad5ab8f Add codes to _looper();
Rewrite _tcp_send(to_all=True)
2023-07-15 19:00:21 +03:00
aa5725e8a5 Minor fix 2023-07-15 18:03:22 +03:00
939723acdd Add event player_join 2023-07-15 17:54:59 +03:00
90beaf1302 Refactor class Client to protected funcs and vars 2023-07-15 17:54:08 +03:00
ee366a2d23 Update TODOs 2023-07-15 17:04:12 +03:00
d665021479 i18n: Update en translate 2023-07-15 17:01:58 +03:00
13ff3207b2 Update en docs 2023-07-15 16:32:07 +03:00
50b479c396 Update docs 2023-07-15 16:21:43 +03:00
62fa4c6f25 Update docs 2023-07-15 16:20:37 +03:00
f0f8da962e Update async logic;
New except;
2023-07-15 15:37:55 +03:00
840d8fd685 New except 2023-07-15 15:36:59 +03:00
4629fbc43a Update Async logic 2023-07-15 15:36:45 +03:00
e9919459af Update Async logic 2023-07-15 15:34:24 +03:00
5f8b70a2ee Update version 2023-07-15 11:03:05 +03:00
a66f3d8b36 Update TODOs 2023-07-15 11:00:46 +03:00
4c3da30a94 Add new events. 2023-07-15 10:59:50 +03:00
9c52e41b99 Add new logs to class KuiToi;
load:
    Add encoding to open();
    Add plugin attributes error;
    Add plugin.start();
    Add plugin.unload();
    Recreate plugin settings;
    Add async support;
2023-07-15 10:59:33 +03:00
51f960f7c2 Remove print() 2023-07-15 10:53:38 +03:00
0cbed05d68 Move plugins loader to core. 2023-07-15 10:52:58 +03:00
c6c6ec31b0 Add new stock events;
Add async support to call_event();
Add **kwargs t0 call_event();
Sends new data to events;
2023-07-15 10:52:25 +03:00
8feba0e085 Minor update 2023-07-15 09:17:52 +03:00
a5202edf83 Relocate open() in plugins 2023-07-15 02:17:00 +03:00
64ce662d04 Add methods in KuiToi class 2023-07-15 02:15:11 +03:00
08e4f0fcba Update TODOs 2023-07-14 23:11:03 +03:00
99f40eadb0 Add returns from ev.call_events 2023-07-14 22:29:37 +03:00
5a40ab8b05 Add TODOs to code. 2023-07-14 22:27:23 +03:00
4f688d7c02 Update LICENCE 2023-07-14 21:55:19 +03:00
c4fe201b86 Move class Client to self file. 2023-07-14 21:52:24 +03:00
a7a9f367c5 0.2.1 -> 0.2.2 2023-07-14 19:17:21 +03:00
8af4e6527f Update TODOs 2023-07-14 19:14:51 +03:00
7bda3dce29 core.py:
Change default cid to -1 for auth;
    Change client logger;
    Update logic of client.kick();
    Remove "kostil" :)));
    Add client._split_load().....(5h);
    DO SendFile - sending mods.;
    Move sync_resources back;
    Add client.ready for blocking "alive" info;
    Refactor client.remove_me();
    Add name for web Thread;
    Minor fixes;
tcp_server.py:
    From auth always return client;
    Add events call in auth;
    Refactor set_down_rw;
    Minor fixes;
2023-07-14 19:13:13 +03:00
6afe62b68e Move sync_resources logic before insert to clients. 2023-07-13 18:08:47 +03:00
87 changed files with 5459 additions and 1422 deletions

7
.gitignore vendored
View File

@@ -137,4 +137,9 @@ dmypy.json
/src/plugins
/test/
*test.py
logs/
logs/
*.yml
*.toml
/win-ver_info.txt
/output/

View File

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

View File

@@ -1,63 +1,23 @@
# KuiToi-Server
## About
**_[Status: Alpha]_** \
**_[Status: Beta]_** \
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
- [ ] Server core
- [x] BeamMP System
- [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher)
- [X] Player authentication
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] TCP Server part:
- [x] Handle code
- [x] Understanding BeamMP header
- [ ] Upload mods
- [x] Connecting to the world
- [x] Chat
- [x] Players online counter
- [ ] Car state synchronizations _(Codes: We, Vi)_
- [ ] "ABG:" (compressed data)
- [x] Decompress data
- [ ] Vehicle data _(Code: Os)_
- [ ] UDP Server part:
- [ ] Players synchronizations _(Code: Zp)_
- [ ] Ping _(Code: p)_
- [x] Additional:
- [x] Logger
- [x] Just logging
- [x] Log in file
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [ ] _(Deferred)_ Static text (bug)
- [x] Events System
- [x] Call events
- [x] Create custom events
- [ ] Return from events
- [x] Plugins support
- [x] Load Python plugins
- [ ] Load Lua plugins (Original BeamMP compatibility)
- [x] MultiLanguage (i18n support)
- [x] Core
- [x] Console
- [x] WebAPI
- [x] HTTP API Server (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] [Documentation](docs/en/readme.md)
[TODO.md](./TODO.md)
## Installation
1. Install **Python 3.10**
1. Install **Python 3.12**+
2. Clone the repository in a location of your choice with: `git clone -b Stable https://github.com/kuitoi/kuitoi-Server.git`.
3. Change directory into the KuiToi-Server: `cd KuiToi-Server`.
4. Install requirements: `pip install -r requirements.txt`.
@@ -78,6 +38,6 @@ Copyright (c) 2023 SantaSpeen (Maxim Khomutov)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without limitation in the rights to use, copy, modify, merge, publish, and/ or distribute copies of the Software in an educational or personal context, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is granted to sell and/ or distribute copies of the Software in a commercial context, subject to the following conditions:
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change.
- Substantial changes: adding, removing, or modifying large parts, shall be developed in the Software. Reorganizing logic in the software does not warrant a substantial change and received permission from the owner.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

73
TODO.md Normal file
View File

@@ -0,0 +1,73 @@
## TODOs
- [x] Server core:
- [x] BeamMP System:
- [x] Private access (Without key, Direct connect)
- [x] Public access (With key, listing in Launcher)
- [X] Player authentication
- [x] TCP Server part:
- [x] Handle code
- [x] Understanding BeamMP header
- [x] Upload mods
- [x] Connecting to the world
- [x] Chat
- [x] Players online counter
- [x] Packets handled (Recursive finding second packet)
- [x] Client events
- [x] Car synchronizations:
- [x] State packets
- [x] Spawn cars
- [x] Delete cars
- [x] Edit cars
- [x] Reset cars
- [x] "ABG": (compressed data)
- [x] Decompress data
- [x] Compress data
- [x] UDP Server part:
- [x] Ping
- [x] Position synchronizations
- [x] Additional:
- [x] Logger:
- [x] Just logging
- [x] Log in file
- [x] Log history (.1.log, .2.log, ...)
- [x] Console:
- [x] Tabulation
- [x] History
- [x] Autocomplete
- [x] Events System:
- [x] Call events
- [x] Create custom events
- [x] Return from events
- [x] Async support
- [x] Add all events
- [x] MultiLanguage: (i18n support)
- [x] Core
- [x] Console
- [x] WebAPI
- [x] Plugins supports:
- [x] Python part:
- [x] Load Python plugins
- [x] Async support
- [x] KuiToi class
- [x] Client (Player) class
- [x] Lua part: (Original BeamMP compatibility)
- [x] Load Lua plugins
- [x] MP Class
- [x] Util class
- [x] FS class
- [ ] HTTP API Server: (fastapi)
- [x] Stop and Start with core
- [x] Configure FastAPI logger
- [ ] Sync with event system
- [ ] Add methods...
- [ ] RCON System:
- [x] Serving
- [ ] Handle commands
- [x] Client
- [x] AES encryption
- [ ] KuiToi System
- [ ] Servers counter
- [ ] Players counter
- [ ] Etc.
- [ ] [Documentation](./docs)

View File

@@ -0,0 +1,48 @@
{
"": "基础阶段",
"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

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

174
docs/cn/plugins/classes.md Normal file
View File

@@ -0,0 +1,174 @@
# 传递的类
## 值得一读
1. 什么是“*args”和“**kwargs” -> [habr上的文章 ↗](https://habr.com/ru/companies/ruvds/articles/482464/)
## KuiToi
_`kt = KuiToi("PluginName"")`_
### kt.log
_常量_\
返回预配置的记录器
### kt.name
_常量_\
返回插件名称
### kt.dir
_常量_\
返回插件文件夹
### kt.open()
_与open()参数相同_\
在kt.dir中打开文件
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> 作为`event_func`调用的事件名称._\
_`event_func: function` -> 要调用的函数._
`event_func`可以传递普通函数或async - 不需要提前进行await。\
您也可以创建自己的事件,并使用自己的名称注册任意数量的事件。
### kt.call_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> 要调用的事件名称._\
_`*args, **kwargs` -> 要传递给函数的参数._
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> 要调用的事件名称._\
_`*args, **kwargs` -> 要传递给函数的参数._\
_需要用`await`调用_
###### _建议阅读*args, **kwargs链接在开头_
所有事件的数据都以以下格式传递:`{"event_name": event_name, "args": args, "kwargs": kwargs}`\
`args: list` -> 表示传递到事件中的数据数组\
`kwargs: dict` -> 表示传递到事件中的数据字典
数据将以数组形式从所有成功的波动中返回。
### kt.call_lua_event(event_name: str, *args) -> list:
_`event_name: str` -> 要调用的事件名称._\
_`*args` -> 要传递给函数的参数._
添加用于向后兼容性。\
lua函数使用直接传递参数`lua_func(*args)`进行调用。
### kt.get_player([pid: int], [nick: str]) -> Player | None:
_`pid: int` -> Player ID - 玩家标识符._\
_`nick: str` -> Player Nick - 玩家昵称._
该方法通过其`pid``nick`返回玩家对象。\
如果无法找到玩家,则返回 `None`
### kt.get_players() -> List[Player] | list:
该方法返回所有玩家的数组。\
如果没有玩家,则数组将为空。
### kt.players_counter() -> int:
该方法返回在线的玩家数量。
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
_`pid: int` -> Player ID - 玩家标识符._\
_`nick: str` -> Player Nick - 玩家昵称._
该方法通过其`pid``nick`返回玩家对象。
## Player (或 Client)
_`pl = kt.get_player()`_\
_`pl = event_data['kwargs']['player']`_
### pl.log -> Logger
_常量_\
返回预配置的记录器
### pl.addr -> str
_常量_\
返回玩家的 IP 地址
### pl.pid -> int
### pl.cid -> int
_常量_\
返回客户端的 ID _(pid: PlayerId = cid: ClientId)_
### pl.key -> str
_常量_\
返回在身份验证期间传递的密钥
### pl.nick -> str
_变量_\
从 BeamMP 服务器传递的昵称,可以更改,后果未知
### pl.roles -> str
_变量_\
从 BeamMP 服务器传递的角色,可以更改(如果设置了不正确的角色,可能会发生意外情况。)
### pl.guest -> bool
_常量_\
返回玩家是否为游客,从 BeamMP 服务器传递
### pl.identifiers -> dict
_常量_\
标识符,从 BeamMP 服务器传递。
### pl.ready -> bool
_常量由核心更改_\
返回布尔值,如果为 True-> 玩家已下载所有资源,在地图上加载
### pl.cars -> dict
_常量由核心更改_\
按类型返回汽车字典:
```python
{
1: {
"packet": car_packet,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn,
"pos": {
"pos":[0,0,0],
"rvel":[0,0,0],
"rot":[0,0,0],
"vel":[0,0,0],
"tim":0,
"ping":0
}
},
2: ...
}
```
其中 `1` - car_id\
其中 `pkt` - 未处理的从客户端收到的数据包(仅供非常有经验的用户使用)\
其中 `json` - 以 dict 形式存储的已处理的数据包\
其中 `json_ok` - 核心是否能够处理数据包\
其中 `snowman` - 车辆是否为雪人\
其中 `over_spawn` - 车辆是否超过了生成限制(通过插件允许)\
其中 `pos` - 车辆位置(通过 UDP 传递)
### pl.last_position -> dict
_常量由核心更改_
返回玩家的最后位置
### **async** pl.kick([reason: str = "Kicked!"]) -> None
_`reason: str` -> 踢出理由,参数可选,默认值为 `Kicked!`_
将玩家踢出服务器
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
_`message: str` -> 消息文本,不带 "Server:"_
_`to_all: bool` -> 是否向所有人发送此消息?参数可选,默认值为 `True`_
向玩家或所有人发送消息
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
_`event_name: str` -> 要调用的事件名称_
_`event_data: Any` -> 发送到事件的数据。_
_`to_all: bool` -> 是否向所有人发送此消息?参数可选,默认值为 `True`_
将事件发送到客户端。\
如果 event_data 是 tuple、list、dict则核心会通过 json.dumps(event_data) 将其转换为 json然后再发送。\
否则,数据将是字符串,不受限制;
### **async** pl.delete_car(self, car_id: int) -> None
_`car_id: int` -> 要删除的车辆的 ID_
删除玩家的车辆

View File

@@ -0,0 +1,13 @@
# 所有可用事件列表
大多数事件将包含`pl = data ['kwargs'] ['player']`,可以在[这里](./classes.md)找到描述。
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
print("example.lua")
--CreateTimer Testing
local mytimer = MP.CreateTimer()
--.--.--.--.--.--.--.
--GetOSName Testing
print("OS Name: "..MP.GetOSName())
--.--.--.--.--.--.-
--GetServerVersion Testing
local major, minor, patch = MP.GetServerVersion()
print("Server Version: "..major.."."..minor.."."..patch)
--.--.--.--.--.--.--.--.--
--Events Testing--
function handleChat(player_id, player_name, message)
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
return 1
end
MP.RegisterEvent("onChatMessage", "handleChat")
--.--.--.--.--.--.
function onInit()
print("Initializing ready!")
end

View File

@@ -0,0 +1,23 @@
# BeamMP Lua反馈支持
KiuToi几乎完全支持BeamMP的lua插件所有必要的方法都已经创建测试显示以下细节
在KiuToi中没有支持:`MP.Set()`
#### Economic Rework V2.0付费DiscordRU[Hlebushek](https://discordapp.com/users/449634697593749516)
1. 要获取`pluginPath`,需要:`debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")`,因为路径返回值中包含`@`,这破坏了插件。
#### 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. 扫描`plugins/`文件夹
2. 如果文件夹不在PyPlugins中并且文件夹中存在`*.lua`,则添加它,例如`plugins/LuaPlugin`
3. 然后从该文件夹中进行`lua.loadfile({filename})`这是lua中的标准方法
4. 最后调用事件和函数`onInit()`
5. 如果在执行`onInit()`期间没有发生错误,则可以通过`lua_plugins`命令看到这样的消息:`Lua plugins: LuaPlugin:ok`

97
docs/cn/plugins/readme.md Normal file
View File

@@ -0,0 +1,97 @@
# 插件系统
### 事件:[这里](./events_list.md)
### 类:[这里](./classes.md)
## 使用带有“Dummy”的库
###### 这意味着它没有服务器无法工作但IDE将指导API
###### (库还在开发中)
* 使用pip\
`$ pip install KuiToi`
* 从源代码安装:\
`git clone https://github.com/KuiToi/KuiToi-PyLib`
## 示例
```python
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("ExamplePlugin")
log = kt.log
def my_event_handler(event_data):
log.info(f"{event_data}")
def load():
# 初始化插件
ev.register("my_event", my_event_handler)
log.info("插件已成功加载。")
def start():
# 启动插件进程
ev.call_event("my_event")
ev.call_event("my_event", "一些数据", data="一些数据也是")
log.info("插件已成功启动。")
def unload():
# 结束所有进程的代码
log.info("插件已成功卸载。")
```
您还可以在[example.py](examples/example.py)中找到更广泛的示例。
* 建议在`load()`后使用`open()`,否则应使用`kt.load()`-在`plugin/<plugin_name>/<filename>`文件夹中创建一个文件
* 创建自己的事件:`kt.register("my_event", my_event_function)`-
* 调用事件:`kt.call_event("my_event")`
* 使用数据调用事件:`kt.call_event("my_event", data, data2=data2)`
* 基本事件_稍后会写_
## 异步函数
支持async
```python
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# 初始化插件
ev.register("my_event", my_event_handler)
log.info("插件已成功加载。")
async def start():
# 启动插件进程
await ev.call_async_event("my_event")
await ev.call_async_event("my_event", "一些数据", data="一些数据也是")
log.info("插件已成功启动。")
async def unload():
# 结束所有进程的代码
log.info("插件已成功卸载。")
```
您还可以在[async_example.py](examples/async_example.py)中找到更广泛的示例。
* 创建自己的事件:`kt.register("my_event", my_event_function)`在register中检查函数
* 调用async事件`kt.call_async_event("my_event")`
* 使用数据调用async事件`kt.call_async_event("my_event", data, data2=data2)`
* 基本的async事件_稍后会写_

10
docs/cn/readme.md Normal file
View File

@@ -0,0 +1,10 @@
# KuiToi服务器文档
### 文档尚未完善,但终有一天会完善
1. 服务器设置和启动 - [这里](./setup)
2. 插件和事件系统 - [这里](./plugins)
3. Lua的细微差别 - [这里](./plugins/lua)
4. 多语言支持 - [这里](./multilanguage)
5. KuiToi WebAPI - [这里](./web)
6. 将会有新的内容...

88
docs/cn/setup/readme.md Normal file
View File

@@ -0,0 +1,88 @@
# 来自 KuiToi 服务器的问候
## 好的,让我们开始吧
###### _(这里是 Linux 的命令)_
* 运行它需要 **Python 3.10.x**只有这个版本才能运行Python 3.11 不支持...
* 您可以像这样检查 Python 版本(你要在这里笑):
```bash
python3 --version # Python 3.10.6
```
* 克隆存储库并导航到它
* 安装所有必需的内容
* 然后,使用我的“脚本”,删除所有不必要的文件并移动到核心源
```bash
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
pip install -r requirements.txt
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
```
* 这是如何检查服务器信息并启动它的方法:
```bash
python3 main.py --help # 显示所有可用命令
python3 main.py # 启动服务器
```
## 配置
* 启动后,将创建 `kuitoi.yaml`
* 默认情况下,它如下所示:
```yaml
!!python/object:modules.ConfigProvider.config_provider.Config
Auth:
key: null
private: true
Game:
map: gridmap_v2
cars: 1
players: 8
Options:
debug: false
encoding: utf-8
language: en
log_chat: true
speed_limit: 0
use_lua: true
use_queue: false
Server:
description: Welcome to KuiToi Server!
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
WebAPI:
enabled: false
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
server_ip: 127.0.0.1
server_port: 8433
```
### Auth
* 如果您将 `private: false` 并且不设置 `key`,服务器将请求一个 BeamMP 密钥,没有它无法启动。
* 输入 BeamMP 密钥后,服务器将出现在启动器列表中。
* 您可以在此处获取密钥:[https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
### Game
* `map` 仅为地图名称,即打开具有地图的 mod 在 `map.zip/levels` - 地图名称将在那里,那就是我们插入的地方。
* `cars` - 每个玩家的最大汽车数量
* `players` - 最大玩家数
### Options
* `debug` - 是否输出调试消息(仅适用于有经验的用户,会略微降低性能)
* `encoding` - 使用哪种编码打开文件
* `language` - 服务器将使用哪种语言启动当前可用enru
* `log_chat` - 是否将聊天输出到控制台
* `speed_limit` - 下载 mod 的下载速度限制(以 MB/s 为单位)
* `use_lua` - 启用 lua 支持
* `use_queue` - 按队列下载 mod即一次只能下载一个客户端
### Server
* `description` - BeamMP 启动器的服务器描述
* `name` - BeamMP 启动器的服务器名称
* `server_ip` - 分配给服务器的 IP 地址(仅适用于有经验的用户,默认为 0.0.0.0
* `server_port` - 服务器将在哪个端口上工作
### WebAPI
##### _文档尚未准备好_

13
docs/cn/web/readme.md Normal file
View File

@@ -0,0 +1,13 @@
# 服务器的 WebAPI
## 可用的端点
* `/stop`:
* 必需参数:
* `secret_key` - 在服务器配置中指定的密钥
* `/event.get`
* 这个端点还没有准备好
* 必需参数:
* `secret_key` - 在服务器配置中指定的密钥

View File

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

View File

@@ -0,0 +1,4 @@
# MultiLanguage - i18n Support
In [example.json](./example.json) you will find a copy of [src/modules/i18n/files/ru.json](../../../src/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.

174
docs/en/plugins/classes.md Normal file
View File

@@ -0,0 +1,174 @@
# Passed Classes
## Worth looking at
1. What are `*args` and `**kwargs`? -> [Post on Habr ↗](https://habr.com/ru/companies/ruvds/articles/482464/)
## KuiToi
_`kt = KuiToi("PluginName"")`_
### kt.log
_Constant_\
Returns a pre-configured logger
### kt.name
_Constant_\
Returns the name of the plugin
### kt.dir
_Constant_\
Returns the directory of the plugin
### kt.open()
_Parameters are the same as for open()_\
Opens a file in kt.dir
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> The name of the event that `event_func` will be called on._\
_`event_func: function` -> The function that will be called._
In `event_func`, you can pass both regular functions and async functions - you don't need to make them async beforehand.\
You can also create your own events with your own names.\
You can register an unlimited number of events.
### kt.call_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args, **kwargs` -> Arguments to be passed to the function._
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args, **kwargs` -> Arguments to be passed to the function._\
_Must be called with `await`_
###### _I recommend familiarizing yourself with *args, **kwargs_, there is a link at the beginning
Data is passed to all events in the form of: `{"event_name": event_name, "args": args, "kwargs": kwargs}`\
`args: list` -> Represents an array of data passed to the event\
`kwargs: dict` -> Represents a dictionary of data passed to the event
The data will be returned from all successful attempts in an array.
### kt.call_lua_event(event_name: str, *args) -> list:
_`event_name: str` -> The name of the event to call._\
_`*args` -> Arguments to be passed to the function._
Added to support backward compatibility.\
The lua function is called with a direct transmission of arguments `lua_func(*args)`
### kt.get_player([pid: int], [nick: str]) -> Player | None:
_`pid: int` -> Player ID - The identifier of the player._\
_`nick: str` -> Player Nickname - The name of the player._
The method returns a player object by their `pid` or `nick`.\
If the player cannot be found, `None` will be returned.
### kt.get_players() -> List[Player] | list:
The method returns an array with all players.\
The array will be empty if there are no players.
### kt.players_counter() -> int:
The method returns the number of players currently online.
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
_`pid: int` -> Player ID - The identifier of the player._\
_`nick: str` -> Player Nickname - The name of the player._
The method returns a player object by their `pid` or `nick`.
## Player (or Client)
_`pl = kt.get_player()`_\
_`pl = event_data['kwargs']['player']`_
### pl.log -> Logger
_Constant_\
Returns a preconfigured logger.
### pl.addr -> str
_Constant_\
Returns the player's IP address.
### pl.pid -> int
### pl.cid -> int
_Constant_\
Returns the client ID _(pid: PlayerId = cid: ClientId)_.
### pl.key -> str
_Constant_\
Returns the key passed during authorization.
### pl.nick -> str
_Variable_\
Nickname passed during authorization from the BeamMP server, can be changed, consequences are not tested.
### pl.roles -> str
_Variable_\
Role passed during authorization from the BeamMP server, can be changed (If the wrong role is set, unexpected behavior may occur.)
### pl.guest -> bool
_Constant_\
Returns whether the player is a guest, passed during authorization from the BeamMP server.
### pl.identifiers -> dict
_Constant_\
Identifiers passed during authorization from the BeamMP server.
### pl.ready -> bool
_Constant, changed by the core_\
Returns a bool value, if True -> player has downloaded all resources and loaded on the map.
### pl.cars -> dict
_Constant, changed by the core_\
Returns a dictionary of cars by type:
```python
{
1: {
"packet": car_packet,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn,
"pos": {
"pos":[0,0,0],
"rvel":[0,0,0],
"rot":[0,0,0],
"vel":[0,0,0],
"tim":0,
"ping":0
}
},
2: ...
}
```
Where `1` - car_id\
Where `pkt` - Unprocessed packet that came from the client (For very experienced users)\
Where `json` - Processed packet stored as dict\
Where `json_ok` - Whether the core was able to process the packet\
Where `snowman` - Is the car a snowman\
Where `over_spawn` - Is the car spawned over the limit (Allowed through plugins)\
Where `pos` - Car position (Passed through UDP)
### pl.last_position -> dict
_Constant, changed by the core_\
Returns the player's last position
### **async** pl.kick([reason: str = "Kicked!"]) -> None
_`reason: str` -> Kick reason. Parameter is optional, by default: `Kicked!`_\
Kicks the player from the server.
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
_`message: str` -> Message text, sent without "Server:"_\
_`to_all: bool` -> Should this message be sent to everyone? Parameter is optional, by default: `True`_\
Sends a message to the player or everyone.
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
_`event_name: str` -> Name of the event that will be called_\
_`event_data: Any` -> Data sent to the event._\
_`to_all: bool` -> Should this message be sent to everyone? Parameter is optional, by default: `True`_\
Sends an event to the client.\
If event_data is a tuple, list, dict, then before sending the core converts it to JSON via json.dumps(event_data)\
Otherwise, the data will be a string without regulation.
### **async** pl.delete_car(self, car_id: int) -> None
_`car_id: int` -> Car ID_\
Deletes the player's car.

View File

@@ -0,0 +1,13 @@
# List of available events
Most events will receive `pl = data['kwargs']['player']`, you can find a description [here](./classes.md)
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

@@ -1,36 +0,0 @@
import KuiToi # Import server object
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
log = beam.log # Use logger from server
def on_load():
# When plugin initialization Server uses plugin.load() to load plugin.
# def load(): is really needed
log.info(beam.name)
# Events handlers
def on_started():
# Simple event handler
log.info("Server starting...")
# Simple event register
beam.register_event("on_started", on_started)
def any_func(data=None):
# Custom event handler
log.info(f"Data from any_func: {data}")
# Create custom event
beam.register_event("my_event", any_func)
# Call custom event
beam.call_event("my_event")
beam.call_event("my_event", "Some data")
# This will be an error since any_func accepts only one argument at the input
beam.call_event("my_event", "Some data", "Some data1")

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
print("example.lua")
--CreateTimer Testing
local mytimer = MP.CreateTimer()
--.--.--.--.--.--.--.
--GetOSName Testing
print("OS Name: "..MP.GetOSName())
--.--.--.--.--.--.-
--GetServerVersion Testing
local major, minor, patch = MP.GetServerVersion()
print("Server Version: "..major.."."..minor.."."..patch)
--.--.--.--.--.--.--.--.--
--Events Testing--
function handleChat(player_id, player_name, message)
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
return 1
end
MP.RegisterEvent("onChatMessage", "handleChat")
--.--.--.--.--.--.
function onInit()
print("Initializing ready!")
end

View File

@@ -0,0 +1,23 @@
# Providing Backward Compatibility for BeamMP Lua
KiuToi provides almost full support for lua plugins with BeamMP. All necessary methods have been created, and testing has revealed the following nuances:
KiuToi does not support: `MP.Set()`
#### Economic Rework V2.0 (Paid, Discord (RU): [Hlebushek](https://discordapp.com/users/449634697593749516))
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.
#### 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.
### A Little About How it Works
Plugin loading goes through several stages:
1. The `plugins/` folder is scanned.
2. If the folder is not in PyPlugins and there are `*.lua` files in the folder, then it is added as a plugin folder, let's say it will be `plugins/LuaPlugin`
3. Next, `lua.loadfile({filename})` is performed from this folder (this is the standard method in lua).
4. Finally, the `onInit()` function is called, and an event is triggered.
5. If no errors occur during the execution of `onInit()`, you can see the message `Lua plugins: LuaPlugin:ok` through the `lua_plugins` command.

View File

@@ -1,9 +1,16 @@
# Plugins System
Это описание системы плагинов для KuiToi сервера на Python:
## Install
###### (Lib can't ready to use)
## Events
### Events list: [here](./events_list.md)
* From pip:\
## Classes
### Classes list: [here](./classes.md)
## Installing the library with "stubs"
###### (This means it won't work without the server, but your IDE will suggest the API)
###### (The library is still in development)
* Using pip:\
`$ pip install KuiToi`
* From source:\
`git clone https://github.com/KuiToi/KuiToi-PyLib`
@@ -11,22 +18,83 @@
## Example
```python
import KuiToi
try:
import KuiToi
except ImportError:
pass
beam = KuiToi("TestPlugin")
logger = beam.log
kt = KuiToi("ExamplePlugin")
log = kt.log
def load(): # Plugins load from here
print(beam.name)
def my_event_handler(event_data):
log.info(f"{event_data}")
def on_started():
logger.info("Server starting...")
def load():
# Plugin initialization
kt.register("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
beam.register_event("on_started", on_started)
def start():
# Starting plugin processes
kt.call_event("my_event")
kt.call_event("my_event", "Some data", data="some data too")
log.info("Plugin started successfully.")
def unload():
# Code that ends all processes
log.info("Plugin unloaded successfully.")
```
* Basic Events: ['on_started', 'on_auth, 'on_stop']
* Create new event : `beam.register_event("my_event", my_event_function)`
* Call event: `beam.call_event("my_event")`
* Call event with some data: `beam.call_event("my_event", data, data2)`
* Calls _**can't support**_ like this: `beam.call_event("my_event", data=data)`
A more comprehensive example can also be found in [example.py](examples/example.py)
* It is recommended to use `open()` after `load()`, otherwise use `kt.load()` - It creates a file in the `plugin/<plugin_name>/<filename>` folder.
* Creating your own event: `kt.register("my_event", my_event_function)` -
* Calling an event: `kt.call_event("my_event")`
* Calling an event with data: `kt.call_event("my_event", data, data2=data2)`
* Base events: _To be added later_
## Async functions
Async support is available
```python
try:
import KuiToi
except ImportError:
pass
kt = KuiToi("Example")
log = kt.log
async def my_event_handler(event_data):
log.info(f"{event_data}")
async def load():
# Plugin initialization
kt.register("my_event", my_event_handler)
log.info("Plugin loaded successfully.")
async def start():
# Starting plugin processes
await kt.call_async_event("my_event")
await kt.call_async_event("my_event", "Some data", data="some data too")
log.info("Plugin started successfully.")
async def unload():
# Code that ends all processes
log.info("Plugin unloaded successfully.")
```
A more comprehensive example can also be found in [async_example.py](examples/async_example.py)
* Creating your own event: `kt.register("my_event", my_event_function)` (register has a function check)
* Calling an async event: `kt.call_async_event("my_event")`
* Calling an async event with data: `kt.call_async_event("my_event", data, data2=data2)`
* Base async events: _To be added later_

View File

@@ -1,9 +1,10 @@
# Documentation for KuiToi Server
#### The documentation has not been perfected yet, but one day it will definitely happen
### The documentation is not yet perfect, but someday it will be.
1. Setup and Start server - [here](setup)
2. Plugins and Events system - [here](plugins)
3. MultiLanguage - [here](./multilanguage)
4. RESP API - [here](./web)
5. Something new
1. Setup and Launching the Server - [here](./setup)
2. Plugins and Event System - [here](./plugins)
3. Nuances of Working with Lua - [here](./plugins/lua)
4. Multilanguage Support - [here](./multilanguage)
5. KuiToi WebAPI - [here](./web)
6. Something new will be added here soon...

View File

@@ -1,18 +1,32 @@
# Hello from KuiToi Server
# Greetings from KuiToi Server
## Start
## Well, let's start
* Need **Python 3.10.x** to start!
* After cloning use this:
###### _(Here are the commands for Linux)_
* **Python 3.10.x** is required to run it! Only this version works, it won't work on Python 3.11...
* You can check your Python version like this (you have to laugh here):
```bash
$ python3 --version # Python 3.10.6
$ python3 main.py --help # Show help message
$ python3 main.py # Start server
python3 --version # Python 3.10.6
```
* Clone the repository and navigate to it
* Install everything necessary
* Then, using my "script", remove all unnecessary files and move to the core source
```bash
git clone -b Stable https://github.com/kuitoi/KuiToi-Server.git && cd KuiToi-Server
pip install -r requirements.txt
mv ./src/ $HOME/ktsrc/ && rm -rf ./* && mv $HOME/ktsrc/* . && rm -rf $HOME/ktsrc
```
* Here's how you can check server info and start it:
```bash
python3 main.py --help # Shows all available commands
python3 main.py # Starts the server
```
## Setup
## Configuration
* After starting server creating `kuitoi.yaml`; Default:
* After starting, `kuitoi.yaml` will be created
* By default, it looks like this:
```yaml
!!python/object:modules.ConfigProvider.config_provider.Config
Auth:
@@ -20,14 +34,55 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Options:
debug: false
encoding: utf-8
language: en
log_chat: true
speed_limit: 0
use_lua: true
use_queue: false
Server:
debug: true
description: This server uses KuiToi!
description: Welcome to KuiToi Server!
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
WebAPI:
enabled: false
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
server_ip: 127.0.0.1
server_port: 8433
```
* Server can't start without BEAM Auth.key
### Auth
* If you set `private: false` and don't set a `key`, the server will request a BeamMP key and won't start without it.
* After entering a BeamMP key, the server will appear in the launcher list.
* You can get the key here: [https://beammp.com/k/keys ↗](https://beammp.com/k/keys)
### Game
* `map` is only the name of the map, i.e. open the mod with the map in `map.zip/levels` - the name of the map will be there, that's what we insert.
* `cars` - Maximum number of cars per player
* `players` - Maximum number of players
### Options
* `debug` - Whether to output debug messages (for experienced users only, slightly reduces performance)
* `encoding` - Which encoding to use to open files
* `language` - Which language the server will start with (currently available: en, ru)
* `log_chat` - Whether to output chat to the console
* `speed_limit` - Download speed limit for mods (in MB/s)
* `use_lua` - Enable lua support
* `use_queue` - Download mods in queue, i.e. only 1 client can download at a time
### Server
* `description` - Server description for the BeamMP launcher
* `name` - Server name for the BeamMP launcher
* `server_ip` - IP address to assign to the server (for experienced users only, defaults to 0.0.0.0)
* `server_port` - On which port the server will work
### WebAPI
##### _Docs are not ready_

View File

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

View File

@@ -1,4 +1,5 @@
# Choose your language
### [English](./en)
### [Русский](./ru)
### [English](./en) (AI translation)
### [Chinese](./cn) (AI translation)

View File

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

175
docs/ru/plugins/classes.md Normal file
View File

@@ -0,0 +1,175 @@
# Передаваемые классы
## Стоит ознакомится
1. Что такое `*args` и `**kwargs`? -> [Пост на habr](https://habr.com/ru/companies/ruvds/articles/482464/)
## KuiToi
_`kt = KuiToi("PluginName"")`_
### kt.log
_Константа_\
Вернёт преднастроенный логгер
### kt.name
_Константа_\
Вернёт имя плагина
### kt.dir
_Константа_\
Вернёт папку плагина
### kt.open()
араметры как у open()_\
Открывает файл в kt.dir
### kt.register(event_name: str, event_func: function)
_`event_name: str` -> Имя ивента, по которому будет вызвана `event_func`._\
_`event_func: function` -> Функция, которая будет вызвана._
В `event_func` можно передавать как обычную функцию, так и async - await не нужно делать заранее.\
Ивенты можно создавать так же свои, со своим именем.\
Зарегистрировать можно не ограниченное кол-во ивентов.
### kt.call_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> Имя ивента, который будет вызван._\
_`*args, **kwargs` -> Аргументы, передаваемые во функции._
### **async** kt.call_async_event(event_name: str, *args, **kwargs) -> list:
_`event_name: str` -> Имя ивента, который будет вызван._\
_`*args, **kwargs` -> Аргументы, передаваемые во функции._\
_Необходимо вызывать с `await`_
###### _Советую ознакомиться с *args, **kwargs_, ссылка есть в начале
Данные во все ивенты приходят по типу: `{"event_name": event_name, "args": args, "kwargs": kwargs}`\
`args: list` -> Представляет из себя массив данных, которые переданы в ивент\
`kwargs: dict` -> Представляет из себя словарь данных, которые переданы в ивент
Данные вернутся от всех удачных волнений в массиве.
### kt.call_lua_event(event_name: str, *args) -> list:
_`event_name: str` -> Имя ивента, который будет вызван._\
_`*args` -> Аргументы, передаваемые во функции._
Добавлено для поддержки обратной совместимости.\
lua функция вызывается с прямой передачей аргументов `lua_func(*args)`
### kt.get_player([pid: int], [nick: str]) -> Player | None:
_`pid: int` -> Player ID - Идентификатор игрока._\
_`nick: str` -> Player Nick - Ник игрока._
Метод возвращает объект игрока по его `pid` или `nick`.\
Если не удалось найти игрока вернётся `None`.
### kt.get_players() -> List[Player] | list:
Метод возвращает массив со всеми игроками.\
Массив будет пустой, если игроков нет.
### kt.players_counter() -> int:
Метод возвращает количество игроков, которые сейчас онлайн.
### kt.is_player_connected([pid: int], [nick: str]) -> bool:
_`pid: int` -> Player ID - Идентификатор игрока._\
_`nick: str` -> Player Nick - Ник игрока._
Метод возвращает объект игрока по его `pid`, `nick`.
## Player (или Client)
_`pl = kt.get_player()`_\
_`pl = event_data['kwargs']['player']`_
### pl.log -> Logger
_Константа_\
Вернёт преднастроенный логгер
### pl.addr -> str
_Константа_\
Вернёт IP адрес игрока
### pl.pid -> int
### pl.cid -> int
_Константа_\
Вернёт id клиента _(pid: PlayerId = cid: ClientId)_
### pl.key -> str
_Константа_\
Вернёт ключ, переданный при авторизации
### pl.nick -> str
еременная_\
Ник, переданные при авторизации от сервера BeamMP, можно изменить, последствия не проверенны
### pl.roles -> str
еременная_\
Роль, переданная при авторизации от сервера BeamMP, можно изменить (Если установить не верную роль, могут происходить неожиданности.)
### pl.guest -> bool
_Константа_\
Вернёт является ли игрок гостем, передаётся при авторизации от сервера BeamMP
### pl.identifiers -> dict
_Константа_\
Идентификаторы, передаются при авторизации от сервера BeamMP.
### pl.ready -> bool
_Константа, меняется ядром_\
Вернёт bool значение, если True -> игрок скачал все ресурсы, прогрузился на карте
### pl.cars -> dict
_Константа, меняется ядром_\
Возвращает словарь автомобилей по типу:
```python
{
1: {
"packet": car_packet,
"json": car_json,
"json_ok": bool(car_json),
"snowman": snowman,
"over_spawn": (snowman and allow_snowman) or over_spawn,
"pos": {
"pos":[0,0,0],
"rvel":[0,0,0],
"rot":[0,0,0],
"vel":[0,0,0],
"tim":0,
"ping":0
}
},
2: ...
}
```
Где `1` - car_id\
Где `pkt` - Необработанный пакет который пришел от клиента (Для очень опытных пользователй) \
Где `json` - Обработанный пакет, хранящийся в виде dict\
Где `json_ok` - Смогло ли ядро обработать пакет\
Где `snowman` - Снеговик ли машина\
Где `over_spawn` - Заспавнена ли машина сверх лимита (Разрешается через плагины)\
Где `pos` - Позиция машины (Передаётся через udp)
### pl.last_position -> dict
_Константа, меняется ядром_
Возвращает последнюю позицию игрока
### **async** pl.kick([reason: str = "Kicked!"]) -> None
_`reason: str` -> Причина кика. Параметр не обязателен, по дефолту: `Kicked!`_
Кикает игрока с сервера
### **async** pl.send_message(message: str, [to_all: bool = True]) -> None
_`message: str` -> Текст сообщения, отправляется без "Server:"_
_`to_all: bool` -> Нужно ли отправить это сообщение всем? Параметр не обязателен, по дефолту: `True`_
Отправляет сообщение игроку или всем сразу
### **async** pl.send_event(event_name: str, event_data: Any, [to_all: bool = True]) -> None
_`event_name: str` -> Имя ивента, который будет вызван_
_`event_data: Any` -> Отправляемые данные в ивент._
_`to_all: bool` -> Нужно ли отправить это сообщение всем? Параметр не обязателен, по дефолту: `True`_
Отправляет ивент на клиент.\
Если event_data будет tuple, list, dict, то перед отправкой ядро конвертирует в json через json.dumps(event_data)
Иначе данные будут строкой, без регуляции;
### **async** pl.delete_car(self, car_id: int) -> None
_`car_id: int` -> Идентификатор машины_
Удалят машину у игрока

View File

@@ -0,0 +1,13 @@
# Список ивентов
В большинство ивентов будет приходить `pl = data['kwargs']['player']`, описание можно найти [тут](./classes.md)
* onPlayerJoin
* onPlayerDisconnect
* playerLoaded
* onChatReceive
* onCarSpawn
* onCarDelete
* onCarEdited
* onCarChanged
* ...

View File

@@ -1,36 +0,0 @@
import KuiToi # Import server object
beam = KuiToi("TestPlugin") # Init plugin with name "TestPlugin"
log = beam.log # Use logger from server
def on_load():
# When plugin initialization Server uses plugin.load() to load plugin.
# def load(): is really needed
log.info(beam.name)
# Events handlers
def on_started():
# Simple event handler
log.info("Server starting...")
# Simple event register
beam.register_event("on_started", on_started)
def any_func(data=None):
# Custom event handler
log.info(f"Data from any_func: {data}")
# Create custom event
beam.register_event("my_event", any_func)
# Call custom event
beam.call_event("my_event")
beam.call_event("my_event", "Some data")
# This will be an error since any_func accepts only one argument at the input
beam.call_event("my_event", "Some data", "Some data1")

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
print("example.lua")
--CreateTimer Testing
local mytimer = MP.CreateTimer()
--.--.--.--.--.--.--.
--GetOSName Testing
print("OS Name: "..MP.GetOSName())
--.--.--.--.--.--.-
--GetServerVersion Testing
local major, minor, patch = MP.GetServerVersion()
print("Server Version: "..major.."."..minor.."."..patch)
--.--.--.--.--.--.--.--.--
--Events Testing--
function handleChat(player_id, player_name, message)
print("Lua handleChat:", player_id, player_name, message, "; Uptime: "..mytimer:GetCurrent())
return 1
end
MP.RegisterEvent("onChatMessage", "handleChat")
--.--.--.--.--.--.
function onInit()
print("Initializing ready!")
end

View File

@@ -0,0 +1,23 @@
# Обеспечение обратной поддержки BeamMP Lua
В KiuToi есть практически полная поддержка lua плагинов с BeamMP, все необходимые методы созданы, тестирование показало следующие нюансы:
В KiuToi не будет поддержки: `MP.Set()`
#### Economic Rework V2.0 (Платный, Discord (RU): [Hlebushek](https://discordapp.com/users/449634697593749516))
1. Для получения `pluginPath` нужно: `debug.getinfo(1).source:gsub("\\","/")` => `debug.getinfo(1).source:gsub("\\","/"):gsub("@", "")` так как пусть возвращается с `@`, что сломало плагин.
#### 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. Сканируется папка `plugins/`
2. Если папки нет в PyPlugins и в папке есть `*.lua`, то она добавляется, допустим это будет `plugins/LuaPlugin`
3. Далее из этой папки проходит `lua.loadfile({filename})` (Это стандартный метод в lua)
4. И в конце вызывается ивент и функция `onInit()`
5. Если во время выполнения `onInit()` не произошло ошибок, можно будет увидеть через команду `lua_plugins` такое сообщение: `Lua plugins: LuaPlugin:ok`

View File

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

View File

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

View File

@@ -34,16 +34,56 @@ Auth:
private: true
Game:
map: gridmap_v2
max_cars: 1
cars: 1
players: 8
Server:
Options:
debug: false
description: This server uses KuiToi!
encoding: utf-8
language: en
log_chat: true
speed_limit: 0
use_lua: true
use_queue: false
Server:
description: Welcome to KuiToi Server!
name: KuiToi-Server
server_ip: 0.0.0.0
server_port: 30814
WebAPI:
enabled: false
secret_key: 3838ccb03c86cdb386b67fbfdcba62d0
server_ip: 127.0.0.1
server_port: 8433
```
### Auth
* Если поставить `private: false` и не установить `key`, то сервер запросит BeamMP ключ, без него не запустится.
* Введя BeamMP ключ сервер появится в списке лаунчера.
* Взять ключ можно тут: [https://beammp.com/k/keys](https://beammp.com/k/keys)
### Game
* `map` указывается только название карты, т.е. открываем мод с картой в `map.zip/levels` - вот тут будет название карты, его мы и вставляем
* `cars` - Максимальное количество машин на игрока
* `players` - Максимально количество игроков
### Options
* `debug` - Нужно ли выводить debug сообщения (только для опытных пользователей, немного теряется в производительности)
* `encoding` - С какой кодировкой открывать файлы
* `language` - С каким языком запустится сервер (Доступные на данный момент: en, ru)
* `log_chat` - Нужно-ли выводить чат в консоль
* `speed_limit` - Ограничение скорости на скачивание модов (В Мб/с)
* `use_lua` - Включить ли поддержку lua
* `use_queue` - Скачивать по очереди, т.е. в один момент может скачивать только 1 клиент
### Server
* `description` - Описания сервера для лаунчера BeamMP
* `name` - Названия сервер для лаунчера BeamMP
* `server_ip` - Какой IP адрес занять серверу (только для опытных пользователей, по умолчанию 0.0.0.0)
* `server_port` - На каком порту будет работать сервер
### WebAPI
##### _Доки не готовы_

View File

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

View File

@@ -1,8 +1,14 @@
PyYAML~=6.0
prompt-toolkit~=3.0.38
aiohttp~=3.8.4
aiohttp~=3.9.5
uvicorn~=0.22.0
fastapi~=0.100.0
starlette~=0.27.0
fastapi~=0.109.1
starlette~=0.36.2
pydantic~=2.0.2
click~=8.1.4
click~=8.1.4
lupa~=2.0
toml~=0.10.2
colorama~=0.4.6
cryptography~=42.0.4
prompt_toolkit~=3.0.47
requests~=2.32.3

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

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

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

@@ -0,0 +1,93 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamReader, StreamWriter, DatagramTransport, Lock
from logging import Logger
from typing import Tuple, List, Dict, Optional, Union, Any
from core import Core, utils
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self._connect_time: float = 0.0
self.__tasks = []
self.__reader = reader
self.__writer = writer
self.__packets_queue = []
self._udp_sock: Tuple[DatagramTransport | None, Tuple[str, int] | None] = (None, None)
self._down_sock: Tuple[StreamReader | None, StreamWriter | None] = (None, None)
self._log = utils.get_logger("client(id: )")
self._addr: Tuple[str, int] = writer.get_extra_info("sockname")
self._loop = asyncio.get_event_loop()
self._core: Core = core
self._cid: int = -1
self._key: str = None
self.nick: str = None
self.roles: str = None
self._guest = True
self.__alive = True
self._synced = False
self._ready = False
self._focus_car = -1
self._identifiers = []
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
self._unicycle: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
self._last_position = {}
self._lock = Lock()
async def __gracefully_kick(self): ...
@property
def _writer(self) -> StreamWriter: ...
@property
def alive(self) -> bool: ...
@property
def log(self) -> Logger: ...
@property
def addr(self) -> Tuple[str, int]: ...
@property
def cid(self) -> int: ...
def pid(self) -> int: ...
@property
def key(self) -> str: ...
@property
def guest(self) -> bool: ...
@property
def synced(self) -> bool: ...
@property
def ready(self) -> bool: ...
@property
def identifiers(self) -> list: ...
@property
def cars(self) -> dict: ...
@property
def focus_car(self):
return self._focus_car
@property
def last_position(self) -> dict: ...
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def send_message(self, message: str | bytes, to_all: bool = True) -> None:...
async def send_event(self, event_name: str, event_data: Any, to_all: bool = True) -> None: ...
async def _send(self, data: bytes | str, to_all: bool = False, to_self: bool = True, to_udp: bool = False, writer: StreamWriter = None) -> None: ...
async def _sync_resources(self) -> None: ...
async def _recv(self, one=False) -> bytes | None: ...
async def _split_load(self, start: int, end: int, d_sock: bool, filename: str, sl: float) -> None: ...
async def _get_cid_vid(self, s: str) -> Tuple[int, int]: ...
async def _spawn_car(self, data: str) -> None: ...
async def delete_car(self, car_id: int) -> None: ...
async def _delete_car(self, raw_data: str = None, car_id: int = None) -> None: ...
async def _edit_car(self, raw_data: str, data: str) -> None: ...
async def _reset_car(self, raw_data: str) -> None: ...
async def _handle_car_codes(self, data: str) -> None: ...
async def _connected_handler(self) -> None: ...
async def _chat_handler(self, data: str) -> None: ...
async def _handle_codes(self, data: bytes) -> None: ...
async def _looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def _remove_me(self) -> None: ...

View File

@@ -1,25 +1,23 @@
# Developed by KuiToi Dev
# File core.__init__.py
# Written by: SantaSpeen
# Version 1.3
# Core version: 0.2.1
# Version 1.5
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
# Special thanks to: AI Sage(https://poe.com/Sage), AI falcon-40b-v7(https://OpenBuddy.ai)
# (c) kuitoi.su 2024
__title__ = 'KuiToi-Server'
__description__ = 'BeamingDrive Multiplayer server compatible with BeamMP clients.'
__url__ = 'https://github.com/kuitoi/kuitoi-Server'
__version__ = '0.2.1'
__build__ = 874
__version__ = '0.4.6'
__build__ = 2421 # Я это считаю лог файлами
__author__ = 'SantaSpeen'
__author_email__ = 'admin@kuitoi.su'
__license__ = "FPA"
__copyright__ = 'Copyright 2023 © SantaSpeen (Maxim Khomutov)'
__copyright__ = 'Copyright 2024 © SantaSpeen (Maxim Khomutov)'
import asyncio
import builtins
import os
import webbrowser
import prompt_toolkit.shortcuts as shortcuts
@@ -27,11 +25,11 @@ import prompt_toolkit.shortcuts as shortcuts
from .utils import get_logger
from core.core import Core
from main import parser
from modules import ConfigProvider, EventsSystem, PluginsLoader
from modules import ConfigProvider, EventsSystem
from modules import Console
from modules import MultiLanguage
args = parser.parse_args()
args, _ = parser.parse_known_args()
if args.version:
print(f"{__title__}:\n\tVersion: {__version__}\n\tBuild: {__build__}")
exit(0)
@@ -45,24 +43,24 @@ config_path = "kuitoi.yml"
if args.config:
config_path = args.config
config_provider = ConfigProvider(config_path)
config = config_provider.open_config()
config = config_provider.read()
builtins.config = config
if config.Server['debug'] is True:
config.enc = config.Options['encoding']
if config.Options['debug'] is True:
utils.set_debug_status()
log.info("Debug enabled!")
log = get_logger("core.init")
log.debug("Debug mode enabled!")
log.debug(f"Server config: {config}")
log.info("Debug mode enabled!")
log.debug(f"Server config: {config}")
# i18n init
log.debug("Initializing i18n...")
ml = MultiLanguage()
ml.set_language(args.language or config.Server['language'])
ml.set_language(args.language or config.Options['language'])
ml.builtins_hook()
log.debug("Initializing EventsSystem...")
ev = EventsSystem()
ev.builtins_hook()
ev.register("get_version", lambda _: {"version": __version__, "build": __build__})
log.info(i18n.hello)
log.info(i18n.config_path.format(config_path))
@@ -71,7 +69,7 @@ log.debug("Initializing BeamMP Server system...")
# Key handler..
if not config.Auth['private'] and not config.Auth['key']:
log.warn(i18n.auth_need_key)
url = "https://beammp.com/k/keys"
url = "https://keymaster.beammp.com/login"
if shortcuts.yes_no_dialog(
title='BeamMP Server Key',
text=i18n.GUI_need_key_message,
@@ -93,7 +91,7 @@ if not config.Auth['private'] and not config.Auth['key']:
text=i18n.GUI_enter_key_message,
ok_text=i18n.GUI_ok,
cancel_text=i18n.GUI_cancel).run()
config_provider.save_config()
config_provider.save()
if not config.Auth['private'] and not config.Auth['key']:
log.error(i18n.auth_empty_key)
log.info(i18n.stop)
@@ -103,20 +101,12 @@ if not config.Auth['private'] and not config.Auth['key']:
log.debug("Initializing console...")
console = Console()
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("exit", console.stop, i18n.man_message_exit, i18n.help_message_exit)
log.debug("Initializing PluginsLoader...")
if not os.path.exists("plugins"):
os.mkdir("plugins")
pl = PluginsLoader("plugins")
pl.load_plugins()
builtins.B = 1
builtins.KB = B * 1024
builtins.MB = KB * 1024
builtins.GB = MB * 1024
builtins.TB = GB * 1024
log.info(i18n.init_ok)

View File

@@ -1,210 +1,35 @@
# Developed by KuiToi Dev
# File core.core.py
# Written by: SantaSpeen
# Core version: 0.2.1
# Version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import math
import os
import random
import zlib
import time
from threading import Thread
import aiohttp
import uvicorn
from core import utils
from core import utils, __version__
from core.Client import Client
from core.tcp_server import TCPServer
from core.udp_server import UDPServer
from modules import PluginsLoader
from modules.WebAPISystem import app as webapp
class Client:
def __init__(self, reader, writer, core):
self.reader = reader
self.writer = writer
self.down_rw = (None, None)
self.log = utils.get_logger("client(None:0)")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid = 0
self.key = None
self.nick = None
self.roles = None
self.guest = True
self.alive = True
def _update_logger(self):
self.log = utils.get_logger(f"client({self.nick}:{self.cid})")
self.log.debug(f"Update logger")
def is_disconnected(self):
if not self.alive:
return True
res = self.writer.is_closing()
if res:
self.log.debug(f"Client Disconnected")
self.alive = False
return True
else:
self.log.debug(f"Client Alive")
self.alive = True
return False
async def kick(self, reason):
self.log.info(f"Client: \"IP: {self.addr!r}; ID: {self.cid}\" - kicked with reason: \"{reason}\"")
await self.tcp_send(b"K" + bytes(reason, "utf-8"))
self.alive = False
await self.remove_me()
async def tcp_send(self, data, to_all=False, writer=None):
# TNetwork.cpp; Line: 383
# BeamMP TCP protocol sends a header of 4 bytes, followed by the data.
# [][][][][][]...[]
# ^------^^---...-^
# size data
if writer is None:
writer = self.writer
if to_all:
for client in self.Core.clients:
if not client:
continue
await client.tcp_send(data)
return
self.log.debug(f"tcp_send({data})")
if len(data) == 10:
data += b"."
header = len(data).to_bytes(4, "little", signed=True)
self.log.debug(f'len(data) {len(data)}; send {header + data}')
try:
writer.write(header + data)
await writer.drain()
except ConnectionError:
self.log.debug('Disconnected')
self.alive = False
async def recv(self, kostil=False):
# if not self.is_disconnected():
# self.log.debug(f"Client with {self.nick}({self.cid}) disconnected")
# return b""
try:
header = await self.reader.read(4) # header: 4 bytes
int_header = 0
for i in range(len(header)):
int_header += header[i]
if int_header <= 0:
await asyncio.sleep(0.1)
if not self.alive:
self.log.debug(f"Disconnected")
self.writer.close()
return b''
if kostil:
return
self.log.debug(f"Header: {header}")
await self.kick("Invalid packet - header negative")
return b""
if int_header > 100 * MB:
await self.kick("Header size limit exceeded")
self.log.warn(f"Client {self.nick}({self.cid}) sent header of >100MB - "
f"assuming malicious intent and disconnecting the client.")
return b""
data = await self.reader.read(100 * MB)
self.log.debug(f"header: `{header}`; int_header: `{int_header}`; data: `{data}`;")
if len(data) != int_header:
self.log.debug(f"WARN Expected to read {int_header} bytes, instead got {len(data)}")
abg = b"ABG:"
if len(data) > len(abg) and data.startswith(abg):
data = zlib.decompress(data[len(abg):])
self.log.debug(f"ABG: {data}")
return data
return data
except ConnectionError:
self.alive = False
await self.remove_me()
return b""
async def sync_resources(self):
while True:
data = await self.recv()
if not data:
await asyncio.sleep(.1)
continue
self.log.debug(f"Received: {data}")
if data.startswith(b"f"):
# TODO: SendFile
file = data[1:].decode("utf-8")
self.log.debug(f"Sending File: {file}")
await self.kick(f"TODO: SendFile({file})")
elif data.startswith(b"SR"):
self.log.debug("Sending Mod Info")
mod_list = '%s;%s;'
for mod in self.Core.mods_list:
if type(mod) == int:
continue
mod_list = (mod_list % (mod['path'], mod['size'])).replace(";", ";%s;")
mod_list = mod_list.replace("%s;", "")
if len(mod_list) == 0:
await self.tcp_send(b"-")
else:
await self.tcp_send(bytes(mod_list, "utf-8"))
data = await self.recv()
if data == b"Done":
await self.tcp_send(b"M/levels/" + bytes(config.Game['map'], 'utf-8') + b"/info.json")
break
async def looper(self):
# self.is_disconnected()
await self.tcp_send(b"P" + bytes(f"{self.cid}", "utf-8"))
await self.sync_resources()
while self.alive:
data = await self.recv()
if data == b"":
if not self.alive:
break
else:
await asyncio.sleep(.2)
continue
code = data.decode()[0]
self.log.debug(f"Received code: {code}, data: {data}")
match code:
case "H":
# Client connected
await self.tcp_send(b"Sn" + bytes(self.nick, "utf-8"), to_all=True)
case "C":
# Chat
await self.tcp_send(data, to_all=True)
async def remove_me(self):
self.log.debug(f"Removing client {self.nick}({self.cid})")
await asyncio.sleep(0.3)
if not self.writer.is_closing():
self.writer.close()
if (self.cid > 0 or self.nick is not None) and self.Core.clients_by_id.get(self.cid):
_, down_w = self.down_rw
if down_w and not down_w.is_closing():
down_w.close()
self.Core.clients[self.cid] = None
self.Core.clients_by_id.pop(self.cid)
self.Core.clients_by_nick.pop(self.nick)
# noinspection PyProtectedMember
class Core:
def __init__(self):
self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.start_time = time.monotonic()
self.run = False
self.direct = False
self.clients = []
@@ -220,11 +45,20 @@ class Core:
self.web_pool = webapp.data_pool
self.web_stop = None
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
self.BeamMP_version = "3.4.1" # 16.07.2024
ev.register("_get_BeamMP_version", lambda x: tuple([int(i) for i in self.BeamMP_version.split(".")]))
ev.register("_get_player", lambda x: self.get_client(**x['kwargs']))
def get_client(self, cid=None, nick=None):
if cid:
if cid is None and nick is None:
return None
if cid is not None:
if cid == -1:
return [i for i in self.clients if i is not None and i.synced]
return self.clients_by_id.get(cid)
if nick:
return self.clients_by_nick.get(nick)
@@ -241,11 +75,12 @@ class Core:
break
await asyncio.sleep(random.randint(3, 9) * 0.01)
if not self.clients[cid]:
client.cid = cid
client._cid = cid
self.clients_by_nick.update({client.nick: client})
self.log.debug(f"Inserting client: id{client.cid}")
self.log.debug(f"Inserting client: {client.nick}:{client.cid}")
self.clients_by_id.update({client.cid: client})
self.clients[client.cid] = client
# noinspection PyProtectedMember
client._update_logger()
return
await self.insert_client(client)
@@ -269,18 +104,33 @@ class Core:
return out
async def check_alive(self):
self.log.debug("Starting alive checker.")
maxp = config.Game['players']
while self.run:
await asyncio.sleep(1)
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
for client in self.clients:
if not client:
continue
await client.tcp_send(bytes(ca, "utf-8"))
try:
while self.run:
await asyncio.sleep(1)
ca = f"Ss{len(self.clients_by_id)}/{maxp}:{self.get_clients_list()}"
for client in self.clients:
if not client:
continue
if not client.ready:
client.is_disconnected()
continue
if not client.alive:
await client.kick("You are not alive!")
await client._send(ca)
except Exception as e:
self.log.error("Error in check_alive.")
self.log.exception(e)
async def __gracefully_kick(self):
for client in self.clients:
if not client:
continue
await client.kick("Server shutdown!")
@staticmethod
def start_web():
global uvserver
uvconfig = uvicorn.Config("modules.WebAPISystem.app:web_app",
host=config.WebAPI["server_ip"],
port=config.WebAPI["server_port"],
@@ -290,6 +140,8 @@ class Core:
uvserver.run()
async def stop_me(self):
if not config.WebAPI['enabled']:
return
while webapp.data_run[0]:
await asyncio.sleep(1)
self.run = False
@@ -297,99 +149,142 @@ class Core:
# noinspection SpellCheckingInspection,PyPep8Naming
async def heartbeat(self, test=False):
if config.Auth["private"] or self.direct:
if test:
self.log.info(f"Server runnig in Direct connect mode.")
self.direct = True
return
try:
self.log.debug("Starting heartbeat.")
if config.Auth["private"] or self.direct:
if test:
self.log.info(i18n.core_direct_mode)
self.direct = True
return
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
modlist = ""
for mod in self.mods_list:
if type(mod) == int:
continue
modlist += f"/{os.path.basename(mod['path'])};"
modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1
while self.run:
try:
data = {"uuid": config.Auth["key"], "players": len(self.clients), "maxplayers": config.Game["players"],
"port": config.Server["server_port"], "map": f"/levels/{config.Game['map']}/info.json",
"private": config.Auth['private'], "version": self.BeamMP_version,
"clientversion": self.client_major_version,
"name": config.Server["name"], "modlist": modlist, "modstotalsize": modstotalsize,
"modstotal": modstotal, "playerslist": "", "desc": config.Server['description'], "pass": False}
self.log.debug(f"Auth: data {data}")
BEAM_backend = ["backend.beammp.com", "backup1.beammp.com", "backup2.beammp.com"]
_map = config.Game['map'] if "/" in config.Game['map'] else f"/levels/{config.Game['map']}/info.json"
tags = config.Server['tags'].replace(", ", ";").replace(",", ";")
if tags and tags[-1:] != ";":
tags += ";"
modlist = "".join(f"/{os.path.basename(mod['path'])};" for mod in self.mods_list[1:])
modstotalsize = self.mods_list[0]
modstotal = len(self.mods_list) - 1
while self.run:
playerslist = "".join(f"{client.nick};" for client in self.clients if client and client.alive)
data = {
"uuid": config.Auth["key"],
"players": len(self.clients_by_id),
"maxplayers": config.Game["players"],
"port": config.Server["server_port"],
"map": _map,
"private": config.Auth['private'],
"version": self.BeamMP_version,
"clientversion": self.client_major_version,
"name": config.Server["name"],
"tags": tags,
"guests": not config.Auth["private"],
"modlist": modlist,
"modstotalsize": modstotalsize,
"modstotal": modstotal,
"playerslist": playerslist,
"desc": config.Server['description'],
"pass": False
}
# Sentry?
ok = False
body = {}
code = 0
for server_url in BEAM_backend:
url = "https://" + server_url + "/heartbeat"
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, data=data, headers={"api-v": "2"}) as response:
code = response.status
# text = await response.text()
# self.log.debug(f"[HB] res={text}")
body = await response.json()
self.log.debug(f"Auth: code {code}, body {body}")
ok = True
break
except Exception as e:
self.log.debug(f"Auth: Error `{e}` while auth with `{server_url}`")
continue
if ok:
if body:
if not (body.get("status") is not None and
body.get("code") is not None and
body.get("msg") is not None):
self.log.error("Missing/invalid json members in backend response")
raise KeyboardInterrupt
self.log.error(i18n.core_auth_server_error)
return
if test:
status = body.get("status")
msg = body.get("msg")
if status == "2000":
self.log.info(f"Authenticated! {msg}")
elif status == "200":
self.log.info(f"Resumed authenticated session. {msg}")
else:
self.log.error(f"Backend REFUSED the auth key. Reason: "
f"{msg or 'Backend did not provide a reason'}")
self.log.info(f"Server still runnig, but only in Direct connect mode.")
self.direct = True
status = body.get("status")
msg = body.get("msg")
if status == "2000":
if test:
self.log.debug(f"Authenticated! {msg}")
elif status == "200":
if test:
self.log.debug(f"Resumed authenticated session. {msg}")
else:
self.log.debug(f"Auth: data {data}")
self.log.debug(f"Auth: code {code}, body {body}")
self.log.error(i18n.core_auth_server_refused.format(
msg or i18n.core_auth_server_refused_no_reason))
self.log.info(i18n.core_auth_server_refused_direct_node)
self.direct = True
else:
self.direct = True
if test:
self.log.error("Cannot auth...")
if not config.Auth['private']:
raise KeyboardInterrupt
if test:
self.log.info(f"Server still runnig, but only in Direct connect mode.")
self.log.error(i18n.core_auth_server_no_response)
self.log.info(i18n.core_auth_server_refused_direct_node)
# if not config.Auth['private']:
# raise KeyboardInterrupt
if test:
return ok
return bool(body)
await asyncio.sleep(5)
except Exception as e:
self.log.error(f"Error in heartbeat: {e}")
except Exception as 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):
self.run = True
self.tcp = self.tcp(self, self.server_ip, self.server_port)
self.udp = self.udp(self, self.server_ip, self.server_port)
console.add_command(
"list",
lambda x: f"Players list: {self.get_clients_list(True)}"
)
console.add_command("kick", self.kick_cmd)
pl_dir = "plugins"
self.log.debug("Initializing PluginsLoaders...")
if not os.path.exists(pl_dir):
os.mkdir(pl_dir)
pl = PluginsLoader(pl_dir)
await pl.load()
if config.Options['use_lua']:
from modules.PluginsLoader.lua_plugins_loader import LuaPluginsLoader
lpl = LuaPluginsLoader(pl_dir)
lpl.load()
try:
# WebApi Start
if config.WebAPI["enabled"]:
self.log.debug("Initializing WebAPI...")
web_thread = Thread(target=self.start_web)
web_thread = Thread(target=self.start_web, name="WebApiThread")
web_thread.start()
self.log.debug(f"WebAPI started at new thread: {web_thread.name}")
self.web_thread = web_thread
# noinspection PyProtectedMember
self.web_stop = webapp._stop
await asyncio.sleep(.3)
@@ -404,40 +299,61 @@ class Core:
self.mods_list.append({"path": path, "size": size})
self.mods_list[0] += size
self.log.debug(f"mods_list: {self.mods_list}")
lmods = len(self.mods_list) - 1
if lmods > 0:
self.log.info(f"Loaded {lmods} mods: {round(self.mods_list[0] / MB, 2)}mb")
len_mods = len(self.mods_list) - 1
if len_mods > 0:
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
self.log.info(i18n.init_ok)
await self.heartbeat(True)
for i in range(int(config.Game["players"] * 1.3)):
for i in range(int(config.Game["players"] * 2.3)): # * 2.3 For down sock and buffer.
self.clients.append(None)
tasks = []
# self.udp.start,
nrtasks = [self.tcp.start, console.start, self.stop_me, self.heartbeat, self.check_alive]
for task in nrtasks:
f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive]
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:
tasks.append(asyncio.create_task(task()))
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
await ev.call_async_event("_plugins_start")
self.run = True
self.log.info(i18n.start)
ev.call_event("on_started")
await t
# Wait the end.
ev.call_event("onServerStarted")
await ev.call_async_event("onServerStarted")
await t # Wait end.
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Exception: {e}")
self.log.exception(e)
except KeyboardInterrupt:
pass
finally:
self.tcp.stop()
# self.udp.stop()
self.run = False
self.tcp.stop()
self.udp._stop()
await self.stop()
def start(self):
asyncio.run(self.main())
def stop(self):
async def stop(self):
ev.call_lua_event("onShutdown")
ev.call_event("onServerStopped")
await ev.call_async_event("onServerStopped")
await self.__gracefully_kick()
if config.Options['use_lua']:
ev.call_event("_lua_plugins_unload")
await ev.call_async_event("_plugins_unload")
self.run = False
self.log.info(i18n.stop)
if config.WebAPI["enabled"]:
asyncio.run(self.web_stop())
exit(0)
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,46 +1,23 @@
# Developed by KuiToi Dev
# File core.core.pyi
# Written by: SantaSpeen
# Version 0.2.0
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
from asyncio import StreamWriter, StreamReader
import time
from threading import Thread
from typing import Callable, List, Dict, Tuple
from typing import Callable, List, Dict
from core import utils
from .Client import Client
from .tcp_server import TCPServer
from .udp_server import UDPServer
class Client:
def __init__(self, reader: StreamReader, writer: StreamWriter, core: Core) -> "Client":
self.reader = reader
self.writer = writer
self.down_rw: Tuple[StreamReader, StreamWriter] | Tuple[None, None] = (None, None)
self.log = utils.get_logger("client(id: )")
self.addr = writer.get_extra_info("sockname")
self.loop = asyncio.get_event_loop()
self.Core = core
self.cid: int = 0
self.key: str = None
self.nick: str = None
self.roles: str = None
self.guest = True
self.alive = True
def is_disconnected(self) -> bool: ...
async def kick(self, reason: str) -> None: ...
async def tcp_send(self, data: bytes, to_all:bool = False, writer: StreamWriter = None) -> None: ...
async def sync_resources(self) -> None: ...
async def recv(self) -> bytes: ...
async def looper(self) -> None: ...
def _update_logger(self) -> None: ...
async def remove_me(self) -> None: ...
class Core:
def __init__(self):
self.start_time = time.monotonic()
self.log = utils.get_logger("core")
self.loop = asyncio.get_event_loop()
self.run = False
@@ -57,16 +34,20 @@ class Core:
self.udp = UDPServer
self.web_thread: Thread = None
self.web_stop: Callable = lambda: None
self.lock_upload = False
self.client_major_version = "2.0"
self.BeamMP_version = "3.2.0"
self.BeamMP_version = "3.4.1"
def get_client(self, cid=None, nick=None) -> Client | None: ...
async def insert_client(self, client: Client) -> None: ...
def create_client(self, *args, **kwargs) -> Client: ...
def get_clients_list(self, need_cid=False) -> str: ...
async def check_alive(self) -> None: ...
async def __gracefully_kick(self): ...
@staticmethod
def start_web() -> None: ...
def stop_me(self) -> None: ...
async def heartbeat(self, test=False) -> None: ...
async def kick_cmd(self, args: list) -> None | str: ...
async def main(self) -> None: ...
def start(self) -> None: ...
def stop(self) -> None: ...
async def stop(self) -> None: ...

View File

@@ -1,19 +1,19 @@
# Developed by KuiToi Dev
# File core.tcp_server.py
# Written by: SantaSpeen
# Core version: 0.2.1
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import traceback
from asyncio import AbstractEventLoop
from threading import Thread
import aiohttp
from core import utils
from modules import RateLimiter
# noinspection PyProtectedMember
class TCPServer:
def __init__(self, core, host, port):
self.log = utils.get_logger("TCPServer")
@@ -22,74 +22,93 @@ class TCPServer:
self.host = host
self.port = port
self.run = False
self.rl = RateLimiter(50, 10, 300)
console.add_command("rl", self.rl.parse_console, None, "RateLimiter menu",
{"rl": {"info": None, "unban": None, "ban": None, "help": None}})
async def auth_client(self, reader, writer):
client = self.Core.create_client(reader, writer)
self.log.info(f"Identifying new ClientConnection...")
data = await client.recv()
self.log.debug(f"recv1 data: {data}")
self.log.info(i18n.core_identifying_connection)
data = await client._recv(True)
self.log.debug(f"Version: {data}")
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
await client.kick("Outdated Version.")
return False, None
await client.kick(i18n.core_player_kick_outdated)
return False, client
else:
await client.tcp_send(b"S") # Accepted client version
# await client._send(b"S") # Accepted client version
await client._send(b"A") # Accepted client version
data = await client.recv()
self.log.debug(f"recv2 data: {data}")
data = await client._recv(True)
self.log.debug(f"Key: {data}")
if len(data) > 50:
await client.kick("Invalid Key (too long)!")
return False, None
client.key = data.decode("utf-8")
await client.kick(i18n.core_player_kick_bad_key)
return False, client
client._key = data.decode("utf-8")
ev.call_event("onPlayerSentKey", player=client)
try:
async with aiohttp.ClientSession() as session:
url = 'https://auth.beammp.com/pkToUser'
async with session.post(url, data={'key': client.key}) as response:
async with session.post(url, data={'key': client._key}) as response:
res = await response.json()
self.log.debug(f"res: {res}")
if res.get("error"):
await client.kick('Invalid key! Please restart your game.')
return False, None
await client.kick(i18n.core_player_kick_invalid_key)
return False, client
client.nick = res["username"]
client.roles = res["roles"]
client.guest = res["guest"]
client._guest = res["guest"]
client._identifiers = {k: v for s in res["identifiers"] for k, v in [s.split(':')]}
if not client._identifiers.get("ip"):
client._identifiers["ip"] = client._addr[0]
# noinspection PyProtectedMember
client._update_logger()
except Exception as e:
self.log.error(f"Auth error: {e}")
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
return False, None
self.log.error("Auth error.")
self.log.exception(e)
await client.kick(i18n.core_player_kick_auth_server_fail)
return False, client
for _client in self.Core.clients:
if not _client:
continue
if _client.nick == client.nick and _client.guest == client.guest:
await client.kick('Stale Client (replaced by new client)')
return False, None
await _client.kick(i18n.core_player_kick_stale)
ev.call_event("on_auth", client)
allow = True
reason = i18n.core_player_kick_no_allowed_default_reason
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
for data in lua_data:
if 1 == data:
allow = False
elif isinstance(data, str):
allow = False
reason = data
if not allow:
await client.kick(reason)
return False, client
ev.call_event("onPlayerAuthenticated", player=client)
if len(self.Core.clients_by_id) > config.Game["players"]:
await client.kick("Server full!")
return False, None
await client.kick(i18n.core_player_kick_server_full)
return False, client
else:
self.log.info("Identification success")
await self.Core.insert_client(client)
client.log.info(i18n.core_identifying_okay)
return True, client
async def set_down_rw(self, reader, writer):
try:
cid = (await reader.read(1))[0]
ok = False
for _client in self.Core.clients:
if not _client:
continue
if _client.cid == cid:
_client.down_rw = (reader, writer)
ok = True
self.log.debug(f"Client: {_client.nick}:{cid} - HandleDownload!")
if not ok:
client = self.Core.get_client(cid=cid)
if client:
client._down_sock = (reader, writer)
self.log.debug(f"Client: {client.nick}:{cid} - HandleDownload!")
else:
writer.close()
self.log.debug(f"Unknown client - HandleDownload")
self.log.debug(f"Unknown client <nick>:{cid} - HandleDownload")
finally:
return
@@ -98,7 +117,7 @@ class TCPServer:
case "C":
result, client = await self.auth_client(reader, writer)
if result:
await client.looper()
await client._looper()
return result, client
case "D":
await self.set_down_rw(reader, writer)
@@ -108,12 +127,18 @@ class TCPServer:
writer.close()
case _:
self.log.error(f"Unknown code: {code}")
self.log.info("Report about that!")
writer.close()
return False, None
async def handle_client(self, reader, writer):
while True:
try:
ip = writer.get_extra_info('peername')[0]
if self.rl.is_banned(ip):
await self.rl.notify(ip, writer)
writer.close()
break
data = await reader.read(1)
if not data:
break
@@ -123,10 +148,10 @@ class TCPServer:
# await asyncio.wait([task], return_when=asyncio.FIRST_EXCEPTION)
_, cl = await self.handle_code(code, reader, writer)
if cl:
await cl.remove_me()
await cl._remove_me()
break
except Exception as e:
self.log.error("Error while connecting..")
self.log.error("Error while handling connection...")
self.log.exception(e)
traceback.print_exc()
break
@@ -136,15 +161,17 @@ class TCPServer:
self.run = True
try:
server = await asyncio.start_server(self.handle_client, self.host, self.port,
backlog=int(config.Game["players"] * 1.3))
backlog=int(config.Game["players"] * 2.3))
self.log.debug(f"TCP server started on {server.sockets[0].getsockname()!r}")
while True:
async with server:
await server.serve_forever()
except OSError as e:
self.log.error("Cannot bind port")
self.log.error(i18n.core_bind_failed.format(e))
raise e
except BaseException as e:
except KeyboardInterrupt:
pass
except Exception as e:
self.log.error(f"Error: {e}")
raise e
finally:

View File

@@ -1,7 +1,7 @@
# Developed by KuiToi Dev
# File core.tcp_server.pyi
# Written by: SantaSpeen
# Version 0.2.0
# Core version: 0.4.5
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
@@ -9,7 +9,8 @@ from asyncio import StreamWriter, StreamReader
from typing import Tuple
from core import utils, Core
from core.core import Client
from core.Client import Client
from modules import RateLimiter
class TCPServer:
@@ -20,6 +21,8 @@ class TCPServer:
self.host = host
self.port = port
self.run = False
self.rl = RateLimiter(50, 10, 15)
async def auth_client(self, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...
async def set_down_rw(self, reader: StreamReader, writer: StreamWriter) -> bool: ...
async def handle_code(self, code: str, reader: StreamReader, writer: StreamWriter) -> Tuple[bool, Client]: ...

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,72 @@
# -*- coding: utf-8 -*-
import copy
# Developed by KuiToi Dev
# File modules.config_provider.__init__.py
# File modules.ConfigProvider
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
from .config_provider import ConfigProvider, Config
import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "tags": "Freroam",
"server_ip": "0.0.0.0", "server_port": 30814}
self.Options = options or {"language": "en", "speed_limit": 0, "use_queue": False,
"use_lua": False, "log_chat": True}
self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383,
"password": secrets.token_hex(6)}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"access_token": secrets.token_hex(16)}
def __repr__(self):
return f"{self.__class__.__name__}(Auth={self.Auth!r}, Game={self.Game!r}, Server={self.Server!r}, " \
f"RCON={self.RCON!r}, Options={self.Options!r}, WebAPI={self.WebAPI!r})"
class ConfigProvider:
def __init__(self, config_path):
self.config_path = config_path
self.config = Config()
def read(self, _again=False):
if not os.path.exists(self.config_path):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)
try:
with open(self.config_path, "r", encoding="utf-8") as f:
self.config = yaml.load(f.read(), yaml.Loader)
except yaml.YAMLError:
print("You have errors in the YAML syntax.")
print("Stopping server.")
exit(1)
if not self.config:
if _again:
print("Error: empty configuration.")
exit(1)
print("Empty config?..")
os.remove(self.config_path)
self.config = Config()
return self.read(True)
if not self.config.Options.get("debug"):
self.config.Options['debug'] = False
if not self.config.Options.get("encoding"):
self.config.Options['encoding'] = "utf-8"
return self.config
def save(self):
_config = copy.deepcopy(self.config)
del _config.enc
del _config.Options['debug']
del _config.Options['encoding']
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(_config, f)

View File

@@ -1,8 +0,0 @@
class Config:
Auth: dict
Game: dict
Server: dict
WebAPI: dict
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ...

View File

@@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.config_provider.config_provider.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import os
import secrets
import yaml
class Config:
def __init__(self, auth=None, game=None, server=None, web=None):
self.Auth = auth or {"key": None, "private": True}
self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1}
self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "language": "en",
"server_ip": "0.0.0.0", "server_port": 30814, "debug": False}
self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433,
"secret_key": secrets.token_hex(16)}
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class ConfigProvider:
def __init__(self, config_path):
self.config_path = config_path
self.config = Config()
def open_config(self):
if not os.path.exists(self.config_path):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)
try:
with open(self.config_path, "r", encoding="utf-8") as f:
self.config = yaml.load(f.read(), yaml.Loader)
except yaml.YAMLError:
print("You have errors in the YAML syntax.")
print("Stopping server.")
exit(1)
return self.config
def save_config(self):
with open(self.config_path, "w", encoding="utf-8") as f:
yaml.dump(self.config, f)

View File

@@ -0,0 +1,15 @@
### Builtins
```python
class Config:
Auth: Dict[str, object]
Game: Dict[str, object]
Server: Dict[str, object]
RCON: Dict[str, object]
Options: Dict[str, object]
WebAPI: Dict[str, object]
enc: str | None
def __repr__(self):
return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server)
class config (Config): ...
```

View File

@@ -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,9 +1,271 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.console.__init__.py
# File modules.ConsoleSystem
# Written by: SantaSpeen
# Version 1.0
# Version 1.2
# Licence: FPA
# (c) kuitoi.su 2023
from .console_system import Console
import builtins
import inspect
import logging
from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory
try:
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
except AssertionError:
class NoConsoleScreenBufferError(Exception): ...
from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger
from modules.ConsoleSystem.RCON import RCONSystem
class Console:
def __init__(self,
prompt_in="> ",
prompt_out="",
not_found="Command \"%s\" not found in alias.",
debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False
self.no_cmd = False
self.__prompt_in = prompt_in
self.__prompt_out = prompt_out
self.__not_found = not_found
self.__is_debug = debug
self.__print = print
self.__func = dict()
self.__alias = dict()
self.__man = dict()
self.__desc = dict()
self.__print_logger = get_logger("print")
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
custom_completer={"man": {}})
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}})
self.completer = NestedCompleter.from_nested_dict(self.__alias)
rcon = RCONSystem
rcon.console = self
self.rcon = rcon
def __debug(self, *x):
self.__logger.debug(f"{x}")
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item):
print(item)
@staticmethod
def __get_max_len(arg) -> int:
i = 0
arg = list(arg)
for a in arg:
ln = len(str(a))
if ln > i:
i = ln
return i
def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0:
return self.__man.get("man")
x = argv[0]
if self.__alias.get(x) is None:
return i18n.man_command_not_found.format(x)
man_message = self.__man.get(x)
if man_message:
return man_message
else:
return i18n.man_message_not_found
# noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr:
self.__debug("creating help message")
raw = False
max_len_v = 0
if "--raw" in argv:
max_len_v = self.__get_max_len(self.__func.values())
print()
raw = True
message = "\n"
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
else:
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
for k, v in self.__func.items():
doc = self.__desc.get(k)
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
else:
if doc is None:
doc = i18n.help_message_not_found
message += f" %-{max_len}s : %s\n" % (k, doc)
return message
def __update_completer(self):
self.completer = NestedCompleter.from_nested_dict(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
key = key.format(" ", "-")
if not isinstance(key, str):
raise TypeError("key must be string")
self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None})
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.__desc.update({key: desc})
self.__update_completer()
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):
if isinstance(s, (list, tuple)):
for text in s:
self._write(text)
else:
self._write(s)
def log(self, s: AnyStr) -> None:
if isinstance(s, (list, tuple)):
for text in s:
self.__logger.info(f"{text}")
else:
self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def read_input(self):
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
with patch_stdout():
if self.no_cmd:
cmd_in = input(self.__prompt_in)
else:
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 = cmd_s[0]
if cmd == "":
continue
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)
if command_object:
func = command_object['f']
if inspect.iscoroutinefunction(func):
out = await func(cmd_s[1:])
else:
out = func(cmd_s[1:])
if out:
self.log(out)
else:
if not found_in_lua:
self.log(self.__not_found % cmd)
except KeyboardInterrupt:
raise KeyboardInterrupt
except Exception as e:
print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self):
self.__is_run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt

View File

@@ -1,39 +0,0 @@
class Console(object):
def __init__(self,
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):
@staticmethod
def alias() -> dict: ...
@staticmethod
def add_command(key: str, func: function) -> dict: ...
@staticmethod
async def start() -> None: ...
@staticmethod
def builtins_hook() -> None: ...
@staticmethod
def logger_hook() -> None: ...
@staticmethod
def log(s: str) -> None: ...
@staticmethod
def write(s: str) -> None: ...
@staticmethod
def __lshift__(s: AnyStr) -> None: ...

View File

@@ -1,223 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File core.config_provider.py
# Written by: SantaSpeen
# Version 1.1
# Licence: FPA
# (c) kuitoi.su 2023
import builtins
import logging
from typing import AnyStr
from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout
from core import get_logger
class Console:
def __init__(self,
prompt_in="> ",
prompt_out="",
not_found="Command \"%s\" not found in alias.",
debug=False) -> None:
self.__logger = get_logger("console")
self.__is_run = False
self.__prompt_in = prompt_in
self.__prompt_out = prompt_out
self.__not_found = not_found
self.__is_debug = debug
self.__print = print
self.__func = dict()
self.__alias = dict()
self.__man = dict()
self.__desc = dict()
self.__print_logger = get_logger("print")
self.add_command("man", self.__create_man_message, i18n.man_message_man, i18n.help_message_man,
custom_completer={"man": {}})
self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help,
custom_completer={"help": {"--raw": None}})
self.completer = NestedCompleter.from_nested_dict(self.__alias)
def __debug(self, *x):
self.__logger.debug(f"{x}")
# if self.__is_debug:
# x = list(x)
# x.insert(0, "\r CONSOLE DEBUG:")
# self.__print(*x)
def __getitem__(self, item):
print(item)
@staticmethod
def __get_max_len(arg) -> int:
i = 0
arg = list(arg)
for a in arg:
ln = len(str(a))
if ln > i:
i = ln
return i
def __create_man_message(self, argv: list) -> AnyStr:
if len(argv) == 0:
return self.__man.get("man")
x = argv[0]
if self.__alias.get(x) is None:
return i18n.man_command_not_found.format(x)
man_message = self.__man.get(x)
if man_message:
return man_message
else:
return i18n.man_message_not_found
# noinspection PyStringFormat
def __create_help_message(self, argv: list) -> AnyStr:
self.__debug("creating help message")
raw = False
max_len_v = 0
if "--raw" in argv:
max_len_v = self.__get_max_len(self.__func.values())
print()
raw = True
message = str()
max_len = self.__get_max_len(self.__func.keys())
if max_len < 7:
max_len = 7
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % ("Key", "Function", "Description")
else:
message += f" %-{max_len}s : %s\n" % (i18n.help_command, i18n.help_message)
for k, v in self.__func.items():
doc = self.__desc.get(k)
if raw:
message += f"%-{max_len}s; %-{max_len_v}s; %s\n" % (k, v, doc)
else:
if doc is None:
doc = i18n.help_message_not_found
message += f" %-{max_len}s : %s\n" % (k, doc)
return message
def __update_completer(self):
self.completer = NestedCompleter.from_nested_dict(self.__alias)
def add_command(self, key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict:
key = key.format(" ", "-")
if not isinstance(key, str):
raise TypeError("key must be string")
self.__debug(f"added user command: key={key}; func={func};")
self.__alias.update(custom_completer or {key: None})
self.__alias["man"].update({key: None})
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.__desc.update({key: desc})
self.__update_completer()
return self.__alias.copy()
def write(self, s: AnyStr):
if s.startswith("html"):
print_formatted_text(HTML(s[4:]))
else:
print_formatted_text(s)
def log(self, s: AnyStr) -> None:
self.__logger.info(f"{s}")
# self.write(s)
def __lshift__(self, s: AnyStr) -> None:
self.write(s)
@property
def alias(self) -> dict:
return self.__alias.copy()
def __builtins_print(self,
*values: object,
sep: str or None = " ",
end: str or None = None,
file: str or None = None,
flush: bool = False) -> None:
self.__debug(f"Used __builtins_print; is_run: {self.__is_run}")
val = list(values)
if len(val) > 0:
if self.__is_run:
self.__print_logger.info(f"{' '.join([''.join(str(i)) for i in values])}\r\n{self.__prompt_in}")
else:
if end is None:
end = "\n"
self.__print(*tuple(val), sep=sep, end=end, file=file, flush=flush)
def logger_hook(self) -> None:
self.__debug("used logger_hook")
def emit(cls, record):
try:
msg = cls.format(record)
if cls.stream.name == "<stderr>":
self.write(f"\r{msg}")
else:
cls.stream.write(msg + cls.terminator)
cls.flush()
except RecursionError:
raise
except Exception as e:
cls.handleError(record)
logging.StreamHandler.emit = emit
def builtins_hook(self) -> None:
self.__debug("used builtins_hook")
builtins.Console = Console
builtins.console = self
# builtins.print = self.__builtins_print
async def read_input(self):
session = PromptSession(history=FileHistory('./.cmdhistory'))
while True:
try:
with patch_stdout():
cmd_in = await session.prompt_async(
self.__prompt_in,
completer=self.completer,
auto_suggest=AutoSuggestFromHistory()
)
cmd_s = cmd_in.split(" ")
cmd = cmd_s[0]
if cmd == "":
continue
else:
command_object = self.__func.get(cmd)
if command_object:
out = command_object['f'](cmd_s[1:])
if out:
self.log(out)
else:
self.log(self.__not_found % cmd)
except KeyboardInterrupt:
raise KeyboardInterrupt
except Exception as e:
print(f"Error in console.py: {e}")
self.__logger.exception(e)
async def start(self):
self.__is_run = True
await self.read_input()
def stop(self, *args, **kwargs):
self.__is_run = False
raise KeyboardInterrupt

View File

@@ -0,0 +1,30 @@
### Builtins
```python
class RCONSystem:
console = None
def __init__(self, key, host, port): ...
async def start(self): ...
async def stop(self): ...
class console:
rcon: RCONSystem = RCONSystem
@staticmethod
def alias() -> dict: ...
@staticmethod
def add_command(key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: ...
@staticmethod
async def start() -> None: ...
@staticmethod
def builtins_hook() -> None: ...
@staticmethod
def logger_hook() -> None: ...
@staticmethod
def log(s: str) -> None: ...
@staticmethod
def write(s: str) -> None: ...
@staticmethod
def __lshift__(s: AnyStr) -> None: ...
```

View File

@@ -1 +1,163 @@
from .events_system import EventsSystem
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.EventsSystem
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import builtins
import inspect
from core import get_logger
# noinspection PyShadowingBuiltins
class EventsSystem:
def __init__(self):
# TODO: default events
self.log = get_logger("EventsSystem")
self.loop = asyncio.get_event_loop()
self.as_tasks = []
self.__events = {
"onServerStarted": [], # No handler
"onPlayerSentKey": [], # Only sync, no handler
"onPlayerAuthenticated": [], # (!) Only sync, With handler
"onPlayerJoin": [], # (!) With handler
"onChatReceive": [], # (!) With handler
"onCarSpawn": [], # (!) With handler
"onCarDelete": [], # (!) With handler (admin allow)
"onCarEdited": [], # (!) With handler
"onCarReset": [], # No handler
"onCarChanged": [], # No handler
"onCarFocusMove": [], # No handler
"onSentPing": [], # Only sync, no handler
"onChangePosition": [], # Only sync, no handler
"onPlayerDisconnect": [], # No handler
"onServerStopped": [], # No handler
}
self.__async_events = {
"onServerStarted": [],
"onPlayerJoin": [],
"onChatReceive": [],
"onCarSpawn": [],
"onCarDelete": [],
"onCarEdited": [],
"onCarReset": [],
"onCarChanged": [],
"onCarFocusMove": [],
"onPlayerDisconnect": [],
"onServerStopped": []
}
self.__lua_events = {
"onInit": [], # onServerStarted
"onShutdown": [], # onServerStopped
"onPlayerAuth": [], # onPlayerAuthenticated
"onPlayerConnecting": [], # No
"onPlayerJoining": [], # No
"onPlayerJoin": [], # onPlayerJoin
"onPlayerDisconnect": [], # onPlayerDisconnect
"onChatMessage": [], # onChatReceive
"onVehicleSpawn": [], # onCarSpawn
"onVehicleEdited": [], # onCarEdited
"onVehicleDeleted": [], # onCarDelete
"onVehicleReset": [], # onCarReset
"onFileChanged": [], # TODO lua onFileChanged
"onConsoleInput": [], # kt.add_command
}
self.register_event = self.register
def builtins_hook(self):
self.log.debug("used builtins_hook")
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(self, event_name, event_func, async_event=False, lua=None):
self.log.debug(f"register(event_name='{event_name}', event_func='{event_func}', "
f"async_event={async_event}, lua_event={lua}):")
if lua:
if event_name not in self.__lua_events:
self.__lua_events.update({str(event_name): [{"func_name": event_func, "lua": lua}]})
else:
self.__lua_events[event_name].append({"func_name": event_func, "lua": lua})
self.log.debug("Register ok")
return
if not callable(event_func):
self.log.error(i18n.events_not_callable.format(event_name, f"kt.add_event(\"{event_name}\", function)"))
return
if async_event or inspect.iscoroutinefunction(event_func):
if event_name not in self.__async_events:
self.__async_events.update({str(event_name): [event_func]})
else:
self.__async_events[event_name].append(event_func)
self.log.debug("Register ok")
else:
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
self.log.debug("Register ok")
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Calling async event: '{event_name}'")
funcs_data = []
if event_name in self.__async_events.keys():
for func in self.__async_events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
data = await func(event_data)
funcs_data.append(data)
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_event()"))
return funcs_data
def call_event(self, event_name, *args, **kwargs):
if event_name not in ["onChangePosition", "onSentPing"]: # UDP events
self.log.debug(f"Calling sync event: '{event_name}'")
funcs_data = []
if event_name in self.__events.keys():
for func in self.__events[event_name]:
try:
event_data = {"event_name": event_name, "args": args, "kwargs": kwargs}
funcs_data.append(func(event_data))
except Exception as e:
self.log.error(i18n.events_calling_error.format(event_name, func.__name__))
self.log.exception(e)
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "kt.call_async_event()"))
return funcs_data
def call_lua_event(self, event_name, *args):
self.log.debug(f"Calling lua event: '{event_name}{args}'")
funcs_data = []
if event_name in self.__lua_events.keys():
for data in self.__lua_events[event_name]:
lua = data['lua']
func_name = data["func_name"]
try:
func = lua.globals()[func_name]
if not func:
self.log.warning(i18n.events_lua_function_not_found.format("", func_name))
continue
fd = func(*args)
funcs_data.append(fd)
except Exception as e:
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
elif not self.is_event(event_name):
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
return funcs_data

View File

@@ -1,46 +0,0 @@
import builtins
from core import get_logger
class EventsSystem:
def __init__(self):
self.__events = {
"on_started": [self.on_started],
"on_stop": [self.on_stop],
"on_auth": [self.on_auth]
}
self.log = get_logger("EventsSystem")
def builtins_hook(self):
self.log.debug("used builtins_hook")
builtins.ev = self
def register_event(self, event_name, event_func):
self.log.debug(f"register_event({event_name}, {event_func}):")
if not callable(event_func):
self.log.error(f"Cannot add event '{event_name}'. "
f"Use `KuiToi.add_event({event_name}', function)` instead. Skipping it...")
return
if event_name not in self.__events:
self.__events.update({str(event_name): [event_func]})
else:
self.__events[event_name].append(event_func)
def call_event(self, event_name, *data):
self.log.debug(f"Using event '{event_name}'")
if event_name in self.__events.keys():
for event in self.__events[event_name]:
event(*data)
else:
self.log.warning(f"Event {event_name} does not exist. Just skipping it...")
def on_started(self):
pass
def on_stop(self):
pass
def on_auth(self, client):
pass

View File

@@ -1,6 +0,0 @@
class EventsSystem:
@staticmethod
def register_event(self, event_name, event_func): ...
@staticmethod
def call_event(self, event_name, *data): ...
class ev(EventsSystem): ...

View File

@@ -0,0 +1,14 @@
### Builtins
```python
class EventsSystem:
@staticmethod
def register(event_name, event_func, async_event: bool = False, lua: bool | object = None): ...
@staticmethod
async def call_async_event(event_name, *args, **kwargs) -> list[Any]: ...
@staticmethod
def call_event(event_name, *data, **kwargs) -> list[Any]: ...
@staticmethod
def call_lua_event(event_name, *data) -> list[Any]: ...
class ev(EventsSystem): ...
```

View File

@@ -1 +1,219 @@
from .plugins_loader import PluginsLoader
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.PluginsLoader
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import asyncio
import inspect
import os
import types
from contextlib import contextmanager
from threading import Thread
from core import get_logger
class KuiToi:
_plugins_dir = ""
def __init__(self, name):
if name is None:
raise AttributeError("KuiToi: Name is required")
self.__log = get_logger(f"Plugin | {name}")
self.__name = name
self.__dir = os.path.join(self._plugins_dir, self.__name)
if not os.path.exists(self.__dir):
os.mkdir(self.__dir)
@property
def log(self):
return self.__log
@property
def name(self):
return self.__name
@property
def dir(self):
return self.__dir
@contextmanager
def open(self, file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
path = os.path.join(self.__dir, file)
self.log.debug(f'Trying to open "{path}" with mode "{mode}"')
# Really need?
# if not os.path.exists(path):
# with open(path, 'x'): ...
f = None
try:
f = open(path, mode, buffering, encoding, errors, newline, closefd, opener)
yield f
except Exception as e:
raise e
finally:
if f is not None:
f.close()
def register(self, event_name, event_func):
self.log.debug(f"Registering event {event_name}")
ev.register(event_name, event_func)
def call_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called event {event_name}")
return ev.call_event(event_name, *args, **kwargs)
async def call_async_event(self, event_name, *args, **kwargs):
self.log.debug(f"Called async event {event_name}")
return await ev.call_async_event(event_name, *args, **kwargs)
def call_lua_event(self, event_name, *args):
self.log.debug(f"Called lua event {event_name}")
return ev.call_lua_event(event_name, *args)
def get_player(self, pid=None, nick=None, cid=None):
self.log.debug("Requests get_player")
return ev.call_event("_get_player", cid=cid or pid, nick=nick)[0]
def get_players(self):
self.log.debug("Requests get_players")
return self.get_player(-1)
def players_counter(self):
self.log.debug("Requests players_counter")
return len(self.get_players())
def is_player_connected(self, pid=None, nick=None):
self.log.debug("Requests is_player_connected")
if pid < 0:
return False
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:
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins = {}
self.plugins_tasks = []
self.plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
self.loaded_str = "Plugins: "
ev.register("_plugins_start", self.start)
ev.register("_plugins_unload", self.unload)
ev.register("_plugins_get", lambda x: list(self.plugins.keys()))
console.add_command("plugins", lambda x: self.loaded_str[:-2])
console.add_command("pl", lambda x: self.loaded_str[:-2])
async def load(self):
self.log.debug("Loading plugins...")
for file in os.listdir(self.plugins_dir):
file_path = os.path.join(self.plugins_dir, file)
if os.path.isfile(file_path) and file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file[:-3]}")
plugin = types.ModuleType(file[:-3])
plugin.KuiToi = KuiToi
plugin.KuiToi._plugins_dir = self.plugins_dir
plugin.print = print
plugin.__file__ = file_path
with open(f'{file_path}', 'r', encoding=config.enc) as f:
code = f.read()
exec(code, plugin.__dict__)
ok = True
try:
is_func = inspect.isfunction
if not is_func(plugin.load):
self.log.error(i18n.plugins_not_found_load)
ok = False
if not is_func(plugin.start):
self.log.error(i18n.plugins_not_found_start)
ok = False
if not is_func(plugin.unload):
self.log.error(i18n.plugins_not_found_unload)
ok = False
if type(plugin.kt) != KuiToi:
self.log.error(i18n.plugins_kt_invalid)
ok = False
except AttributeError:
ok = False
if not ok:
self.log.error(i18n.plugins_invalid.format(file_path))
return
pl_name = plugin.kt.name
if self.plugins.get(pl_name) is not None:
raise NameError(f'Having plugins with identical names is not allowed; '
f'Plugin name: "{pl_name}"; Plugin file "{file_path}"')
plugin.open = plugin.kt.open
is_coro_func = inspect.iscoroutinefunction
self.plugins.update(
{
pl_name: {
"plugin": plugin,
"load": {
"func": plugin.load,
"async": is_coro_func(plugin.load)
},
"start": {
"func": plugin.start,
"async": is_coro_func(plugin.start)
},
"unload": {
"func": plugin.unload,
"async": is_coro_func(plugin.unload)
}
}
}
)
if self.plugins[pl_name]["load"]['async']:
plugin.log.debug(f"I'm async")
await plugin.load()
else:
plugin.log.debug(f"I'm sync")
th = Thread(target=plugin.load, name=f"{pl_name}.load()")
th.start()
th.join()
self.loaded_str += f"{pl_name}:ok, "
self.log.debug(f"Plugin loaded: {file}. Settings: {self.plugins[pl_name]}")
except Exception as e:
self.loaded_str += f"{file}:no, "
self.log.error(i18n.plugins_error_loading.format(file, f"{e}"))
self.log.exception(e)
async def start(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['start']['async']:
self.log.debug(f"Start async plugin: {pl_name}")
t = self.loop.create_task(pl_data['start']['func']())
self.plugins_tasks.append(t)
else:
self.log.debug(f"Start sync plugin: {pl_name}")
th = Thread(target=pl_data['start']['func'], name=f"Thread {pl_name}")
th.start()
self.plugins_tasks.append(th)
except Exception as e:
self.log.exception(e)
async def unload(self, _):
for pl_name, pl_data in self.plugins.items():
try:
if pl_data['unload']['async']:
self.log.debug(f"Unload async plugin: {pl_name}")
await pl_data['unload']['func']()
else:
self.log.debug(f"Unload sync plugin: {pl_name}")
th = Thread(target=pl_data['unload']['func'], name=f"Thread {pl_name}")
th.start()
th.join()
except Exception as e:
self.log.exception(e)

View File

@@ -0,0 +1,100 @@
package.path = package.path..";modules/PluginsLoader/lua_libs/?.lua"
MP.CallStrategy = {
BestEffort = 0,
Precise = 1
}
MP.Settings = {
Debug = 0,
Private = 1,
MaxCars = 2,
MaxPlayers = 3,
Map = 4,
Name = 5,
Description = 6
}
function MP.CreateTimer()
MP.log.debug("request MP.CreateTimer()")
local timer = {}
timer.start_time = os.clock()
function timer:GetCurrent()
return os.clock() - self.start_time
end
function timer:Start()
self.start_time = os.clock()
end
return timer
end
----Timer object for event timers
--local TimedEvent = {}
--TimedEvent.__index = TimedEvent
--
--function TimedEvent:new(interval_ms, event_name, strategy)
-- local o = {}
-- setmetatable(o, self)
-- o.interval = interval_ms
-- o.event_name = event_name
-- o.strategy = strategy or MP.CallStrategy.BestEffort
-- o.last_trigger_time = 0
-- o.timer = MP.CreateTimer()
-- return o
--end
--
--function TimedEvent:trigger()
-- MP.TriggerLocalEvent(self.event_name)
-- self.last_trigger_time = self.timer:GetCurrent()
--end
--
--function TimedEvent:is_ready()
-- local elapsed_time = self.timer:GetCurrent() - self.last_trigger_time
-- return elapsed_time * 1000 >= self.interval
--end
--
---- Event timer management functions
--MP.event_timers = {}
--MP.event_timers_mutex = {}
--
--function MP.CreateEventTimer(event_name, interval_ms, strategy)
-- MP.log.debug("request MP.CreateEventTimer()")
-- strategy = strategy or MP.CallStrategy.BestEffort
-- local timer = TimedEvent:new(interval_ms, event_name, strategy)
-- table.insert(MP.event_timers, timer)
-- MP.log.debug("created event timer for \"" .. event_name .. "\" with " .. interval_ms .. "ms interval")
--end
--
--function MP.CancelEventTimer(event_name)
-- MP.log.debug("request MP.CancelEventTimer()")
-- for i, timer in ipairs(MP.event_timers) do
-- if timer.event_name == event_name then
-- table.remove(MP.event_timers, i)
-- end
-- end
-- MP.log.debug("cancelled event timer for \"" .. event_name .. "\"")
--end
--
--function MP.run_event_timers()
-- MP.log.debug("request MP.run_event_timers()")
-- while true do
-- -- Wait for some time before checking timers
-- MP.Sleep(100)
--
-- -- Check each timer and trigger events as necessary
-- for _, timer in ipairs(MP.event_timers) do
-- if timer:is_ready() then
-- if timer.strategy == MP.CallStrategy.Precise then
-- while timer:is_ready() do
-- timer:trigger()
-- end
-- else
-- timer:trigger()
-- end
-- end
-- end
-- end
--end

View File

@@ -0,0 +1,680 @@
import asyncio
import json
import os
import platform
import random
import re
import shutil
import threading
import time
import toml
from lupa.lua53 import LuaRuntime
from core import get_logger
class EventTimer:
def __init__(self, event_name, interval_ms, mp, strategy=None):
self.log = get_logger(f"EventTimer | {mp.name}")
self.mp = mp
self.event_name = event_name
self.interval_ms = interval_ms
self.strategy = strategy
self.timer = None
self.stopped = False
def start(self):
def callback():
if not self.stopped:
self.start()
self.trigger_event()
self.timer = threading.Timer(self.interval_ms / 1000.0, callback)
self.timer.start()
def stop(self):
self.stopped = True
if self.timer is not None:
self.timer.cancel()
def trigger_event(self):
self.log.debug(f"Event '{self.event_name}' triggered")
self.mp.TriggerLocalEvent(self.event_name)
# noinspection PyPep8Naming,PyProtectedMember
class MP:
def __init__(self, name: str, lua: LuaRuntime):
self.loaded = False
self._event_waiters = []
self.loop = asyncio.get_event_loop()
self.log = get_logger(f"LuaPlugin | {name}")
self.name = name
self.tasks = []
self._lua = lua
self._local_events = {
"onInit": [], "onShutdown": [], "onPlayerAuth": [], "onPlayerConnecting": [], "onPlayerJoining": [],
"onPlayerJoin": [], "onPlayerDisconnect": [], "onChatMessage": [], "onVehicleSpawn": [],
"onVehicleEdited": [], "onVehicleDeleted": [], "onVehicleReset": [], "onFileChanged": []
}
self._event_timers = {}
def _print(self, *args):
args = list(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)):
args[i] = self._lua.globals().Util.JsonEncode(arg)
s = " ".join(map(str, args))
self.log.info(s)
def GetOSName(self) -> str:
self.log.debug("request MP.GetOSName()")
pl = platform.system()
if pl in ["Linux", "Windows"]:
return pl
return "Other"
def GetServerVersion(self) -> tuple[int, int, int]:
self.log.debug("request MP.GetServerVersion()")
return ev.call_event("_get_BeamMP_version")[0]
def RegisterEvent(self, event_name: str, function_name: str) -> None:
self.log.debug("request MP.RegisterEvent()")
ev.register(event_name, function_name, lua=self._lua)
if event_name not in self._local_events:
self._local_events.update({str(event_name): [function_name]})
else:
self._local_events[event_name].append(function_name)
self.log.debug("Register ok (local)")
def CreateEventTimer(self, event_name: str, interval_ms: int, strategy: int = None):
self.log.debug("request CreateEventTimer()")
event_timer = EventTimer(event_name, interval_ms, self, strategy)
self._event_timers[event_name] = event_timer
event_timer.start()
def CancelEventTimer(self, event_name: str):
self.log.debug("request CancelEventTimer()")
if event_name in self._event_timers:
event_timer = self._event_timers[event_name]
event_timer.stop()
del self._event_timers[event_name]
def TriggerLocalEvent(self, event_name, *args):
if event_name != "getTable":
self.log.debug("request TriggerLocalEvent()")
self.log.debug(f"Calling local lua event: '{event_name}{args}'")
funcs_data = []
if event_name in self._local_events.keys():
for func_name in self._local_events[event_name]:
try:
func = self._lua.globals()[func_name]
if not func:
self.log.warning(i18n.events_lua_function_not_found.format(i18n.events_lua_local, func_name))
continue
fd = func(*args)
funcs_data.append(fd)
except Exception as e:
self.log.error(i18n.events_lua_calling_error.format(f"{e}", event_name, func_name, f"{args}"))
else:
self.log.warning(i18n.events_not_found.format(event_name, "ev.call_lua_event(), MP.Trigger<>Event()"))
return self._lua.table_from(funcs_data)
def TriggerGlobalEvent(self, event_name, *args):
self.log.debug("request TriggerGlobalEvent()")
return self._lua.table(
IsDone=lambda: True,
GetResults=lambda: self._lua.table_from(ev.call_lua_event(event_name, *args))
)
def Sleep(self, time_ms):
self.log.debug(f"request Sleep(); Thread: {threading.current_thread().name}")
time.sleep(time_ms * 0.001)
def SendChatMessage(self, player_id, message):
self.log.debug("request SendChatMessage()")
client = ev.call_event("_get_player", cid=player_id)[0]
to_all = False
if player_id < 0:
to_all = True
client = client[0]
if client and message:
t = self.loop.create_task(client.send_message(f"Server: {message}", to_all=to_all))
self.tasks.append(t)
def TriggerClientEvent(self, player_id, event_name, data):
self.log.debug("request TriggerClientEvent()")
client = ev.call_event("_get_player", cid=player_id)[0]
to_all = False
if player_id < 0:
to_all = True
client = client[0] if len(client) > 0 else None
if client and event_name and data:
t = self.loop.create_task(client.send_event(event_name, data, to_all=to_all))
self.tasks.append(t)
return True, None
elif not client:
return False, "Client expired"
else:
return False, "Can't found event_name or data"
def TriggerClientEventJson(self, player_id, event_name, data):
self.log.debug(f"request TriggerClientEventJson({player_id, event_name, data})")
data = self._lua.globals().Util.JsonEncode(data)
self.TriggerClientEvent(player_id, event_name, data)
def GetPlayerCount(self):
self.log.debug("request GetPlayerCount()")
return len(ev.call_event("_get_player", cid=-1)[0])
def GetPositionRaw(self, player_id, car_id):
self.log.debug("request GetPositionRaw()")
if player_id < 0:
return self._lua.table(), "Bad client"
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
car = client._cars[car_id]
if car:
return self._lua.table_from(car['pos'])
return self._lua.table(), "Vehicle not found"
return self._lua.table(), "Client expired"
def IsPlayerConnected(self, player_id):
self.log.debug("request IsPlayerConnected()")
if player_id < 0:
return False
return bool(ev.call_event("_get_player", cid=player_id)[0])
def GetPlayerName(self, player_id):
self.log.debug("request GetPlayerName()")
if player_id < 0:
return None
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
return client.nick
return
def GetPlayerIDByName(self, player_name):
self.log.debug("request GetPlayerIDByName()")
if not isinstance(player_name, str):
return None
client = ev.call_event("_get_player", nick=player_name)[0]
if client:
return client.cid
return
def RemoveVehicle(self, player_id, vehicle_id):
self.log.debug("request RemoveVehicle()")
if player_id < 0:
return
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
t = self.loop.create_task(client._delete_car(car_id=vehicle_id))
self.tasks.append(t)
def GetPlayerVehicles(self, player_id):
self.log.debug("request GetPlayerVehicles()")
if player_id < 0:
return self._lua.table()
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
return self._lua.table_from([f'{v["json"]}' for d in [i for i in client._cars if i is not None]
for k, v in d.items() if k == "json"])
def GetPlayers(self):
self.log.debug("request GetPlayers()")
clients = ev.call_event("_get_player", cid=-1)[0]
return self._lua.table_from([i.nick for i in clients])
def IsPlayerGuest(self, player_id) -> bool:
self.log.debug("request IsPlayerGuest()")
if player_id < 0:
return True
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
return client.guest
return False
def DropPlayer(self, player_id, reason="Kicked"):
self.log.debug("request DropPlayer()")
if player_id < 0:
return
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
t = self.loop.create_task(client.kick(reason))
self.tasks.append(t)
def GetStateMemoryUsage(self):
self.log.debug("request GetStateMemoryUsage()")
return self._lua.get_memory_used()
def GetLuaMemoryUsage(self):
self.log.debug("request GetStateMemoryUsage()")
lua_plugins = ev.call_event("_lua_plugins_get")[0]
return sum(pl['lua'].get_memory_used() for pls in lua_plugins.values() for pl in pls.values())
def GetPlayerIdentifiers(self, player_id):
self.log.debug("request GetStateMemoryUsage()")
client = ev.call_event("_get_player", cid=player_id)[0]
if client:
return self._lua.table_from(client.identifiers)
return self._lua.table()
def Set(self, *_):
self.log.debug("request Set")
self.log.warning("KuiToi cannot support this: MP.Set()")
# noinspection PyPep8Naming
class Util:
def __init__(self, name, lua):
self.log = get_logger(f"LuaPlugin | Util | {name}")
self.name = name
self._lua = lua
def _recursive_list_encode(self, table):
new_list = list(table.values())
for i, v in enumerate(list(table.values())):
if not isinstance(v, (int, float, bool, str, dict, list)) and "LuaTable" not in str(type(v)):
new_list[i] = None
continue
if "LuaTable" in str(type(v)):
d = dict(v)
if all(isinstance(ii, int) for ii in d.keys()):
new_list[i] = self._recursive_list_encode(d)
continue
else:
new_list[i] = self._recursive_dict_encode(d)
return [i for i in new_list if i is not None]
def _recursive_dict_encode(self, table):
new_dict = dict(table)
for k, v in table.items():
if not isinstance(v, (int, float, bool, str, dict, list)) and "LuaTable" not in str(type(v)):
new_dict[k] = None
continue
if "LuaTable" in str(type(v)):
d = dict(v)
if all(isinstance(i, int) for i in d.keys()):
new_dict[k] = self._recursive_list_encode(d)
continue
else:
new_dict[k] = self._recursive_dict_encode(d)
return {k: v for k, v in new_dict.items() if v is not None}
def JsonEncode(self, table):
data = {}
try:
self.log.debug("requesting JsonEncode()")
if all(isinstance(k, int) for k in table.keys()):
data = self._recursive_list_encode(table)
else:
data = self._recursive_dict_encode(table)
except Exception as e:
self.log.exception(e)
data = json.dumps(data)
self.log.debug(f"Encoded: {data}")
return data
def JsonDecode(self, string):
self.log.debug("requesting JsonDecode()")
return self._lua.table_from(json.loads(string))
def JsonPrettify(self, string):
self.log.debug("requesting JsonPrettify()")
data = json.loads(string)
return json.dumps(data, indent=4, sort_keys=True)
def JsonMinify(self, string):
self.log.debug("requesting JsonMinify()")
data = json.loads(string)
return json.dumps(data, separators=(',', ':'))
def JsonFlatten(self, json_str):
self.log.debug("request JsonFlatten()")
json_obj = json.loads(json_str)
flat_obj = {}
def flatten(obj, path=''):
if isinstance(obj, dict):
for key in obj:
flatten(obj[key], path + '/' + key)
elif isinstance(obj, list):
for i in range(len(obj)):
flatten(obj[i], path + '/' + str(i))
else:
flat_obj[path] = obj
flatten(json_obj)
flat_json = json.dumps(flat_obj)
return flat_json
def JsonUnflatten(self, flat_json):
self.log.debug("request JsonUnflatten")
flat_obj = json.loads(flat_json)
def unflatten(obj):
result = {}
for key in obj:
parts = key.split('/')
d = result
for part in parts[:-1]:
if part not in d:
# create a new node in the dictionary
# if the path doesn't exist
d[part] = {}
d = d[part]
# assign the value to the last part of the path
d[parts[-1]] = obj[key]
return result
json_obj = unflatten(flat_obj)
return json.dumps(json_obj)
def JsonDiff(self, a: str, b: str) -> str:
self.log.debug("requesting JsonDiff()")
a_obj = json.loads(a)
b_obj = json.loads(b)
diff = []
for k, v in b_obj.items():
if k not in a_obj:
diff.append({"op": "add", "path": "/" + k, "value": v})
elif a_obj[k] != v:
diff.append({"op": "replace", "path": "/" + k, "value": v})
for k in a_obj.keys() - b_obj.keys():
diff.append({"op": "remove", "path": "/" + k})
return json.dumps(diff)
@staticmethod
def _apply_patch(base_obj, patch_obj):
for patch in patch_obj:
op = patch['op']
path = patch['path']
value = patch.get('value', None)
tokens = path.strip('/').split('/')
obj = base_obj
for i, token in enumerate(tokens):
if isinstance(obj, list):
token = int(token)
if i == len(tokens) - 1:
if op == 'add':
if isinstance(obj, list):
obj.insert(int(token), value)
else:
obj[token] = value
elif op == 'replace':
obj[token] = value
elif op == 'remove':
if isinstance(obj, list):
obj.pop(int(token))
else:
del obj[token]
else:
obj = obj[token]
return base_obj
def JsonDiffApply(self, base: str, diff: str) -> str:
self.log.debug("requesting JsonDiffApply()")
base_obj = json.loads(base)
diff_obj = json.loads(diff)
result = self._apply_patch(base_obj, diff_obj)
return json.dumps(result)
def Random(self) -> float:
self.log.debug("requesting Random()")
return random.random()
def RandomIntRange(self, min_v, max_v) -> int:
self.log.debug("requesting RandomIntRange()")
return random.randint(min_v, max_v)
def RandomRange(self, min_v, max_v) -> float:
self.log.debug("requesting RandomRange()")
return random.uniform(min_v, max_v)
# noinspection PyPep8Naming
class FS:
def __init__(self, name: str, lua: LuaRuntime):
self.log = get_logger(f"LuaPlugin | FP | {name}")
self.name = name
self._lua = lua
def CreateDirectory(self, path):
self.log.debug("requesting CreateDirectory()")
try:
os.makedirs(path)
return True, None
except FileExistsError:
return True, None
except FileNotFoundError | NotADirectoryError as e:
return False, f"{e}"
except PermissionError as e:
return False, f"{e}"
except OSError as e:
return False, f"{e}"
except TypeError as e:
return False, f"{e}"
except ValueError as e:
return False, f"{e}"
def Remove(self, path):
self.log.debug("requesting Remove()")
try:
if os.path.isdir(path):
os.rmdir(path)
else:
os.remove(path)
return True, None
except (FileNotFoundError, NotADirectoryError) as e:
return False, f"{e}"
except PermissionError as e:
return False, f"{e}"
except OSError as e:
return False, f"{e}"
except TypeError as e:
return False, f"{e}"
def Rename(self, path_from, path_to):
self.log.debug("requesting Rename()")
try:
os.rename(path_from, path_to)
return True, None
except (FileNotFoundError, NotADirectoryError) as e:
return False, f"{e}"
except PermissionError as e:
return False, f"{e}"
except OSError as e:
return False, f"{e}"
except TypeError as e:
return False, f"{e}"
def Copy(self, path_from, path_to):
self.log.debug("requesting Copy()")
try:
if os.path.isfile(path_from):
shutil.copy2(path_from, path_to)
elif os.path.isdir(path_from):
shutil.copytree(path_from, path_to)
else:
raise ValueError("Invalid path: {}".format(path_from))
return True, None
except (FileNotFoundError, NotADirectoryError, shutil.Error) as e:
return False, f"{e}"
except PermissionError as e:
return False, f"{e}"
except OSError as e:
return False, f"{e}"
except TypeError as e:
return False, f"{e}"
def GetFilename(self, path):
self.log.debug("requesting GetFilename()")
return os.path.basename(path)
def GetExtension(self, path):
self.log.debug("requesting GetExtension()")
return os.path.splitext(path)[1]
def GetParentFolder(self, path):
self.log.debug("requesting GetParentFolder()")
return os.path.dirname(path)
def Exists(self, path):
self.log.debug("requesting Exists()")
return os.path.exists(path)
def IsDirectory(self, path):
self.log.debug("requesting IsDirectory()")
return os.path.isdir(path)
def IsFile(self, path):
self.log.debug("requesting IsFile()")
return os.path.isfile(path)
def ListDirectories(self, path):
self.log.debug("requesting ListDirectories()")
directories = []
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isdir(item_path):
directories.append(item)
return self._lua.table_from(directories)
def ListFiles(self, path):
self.log.debug("requesting ListFiles()")
files = []
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
files.append(item)
return self._lua.table_from(files)
def ConcatPaths(self, *args):
self.log.debug("requesting ConcatPaths()")
return os.path.join(*args)
# noinspection PyProtectedMember
class LuaPluginsLoader:
def __init__(self, plugins_dir):
self.loop = asyncio.get_event_loop()
self.plugins_dir = plugins_dir
self.lua_plugins = {}
self.lua_plugins_tasks = []
self.lua_dirs = set()
self.log = get_logger("LuaPluginsLoader")
self.loaded_str = "Lua plugins: "
ev.register("_lua_plugins_get", lambda x: self.lua_plugins)
ev.register("_lua_plugins_unload", self.unload)
console.add_command("lua_plugins", lambda x: self.loaded_str[:-2])
console.add_command("lua_pl", lambda x: self.loaded_str[:-2])
def load(self):
self.log.debug("Loading Lua plugins...")
self.log.info(i18n.plugins_lua_enabled)
self.log.warning(i18n.plugins_lua_nuances_warning)
self.log.warning(i18n.plugins_lua_legacy_config_create_warning)
self.log.info(i18n.plugins_lua_legacy_config_create)
data = {
"info": "ServerConfig.toml is created solely for backward compatibility support. "
"This file will be updated every time the program is launched.",
"General": {
"Name": config.Server['name'],
"Port": config.Server['server_port'],
"AuthKey": config.Auth['key'],
"LogChat": config.Options['log_chat'],
"Debug": config.Options['debug'],
"Private": config.Auth['private'],
"MaxCars": config.Game['cars'],
"MaxPlayers": config.Game['players'],
"Map": f"/levels/{config.Game['map']}/info.json",
"Description": config.Server['description'],
"ResourceFolder": "plugins/"
},
"Misc": {
"ImScaredOfUpdates": False,
"SendErrorsShowMessage": False,
"SendErrors": False
},
"HTTP": {
"HTTPServerIP": config.WebAPI['server_ip'],
"HTTPServerPort": config.WebAPI['server_port'],
"SSLKeyPath": None,
"SSLCertPath": None,
"UseSSL": False,
"HTTPServerEnabled": config.WebAPI['enabled'],
}
}
with open("ServerConfig.toml", "w") as f:
toml.dump(data, f)
self.log.warning("KuiToi will not support at all: MP.Set()")
py_folders = ev.call_event("_plugins_get")[0]
for name in os.listdir(self.plugins_dir):
path = os.path.join(self.plugins_dir, name)
if os.path.isdir(path) and name not in py_folders and name not in "__pycache__":
plugin_path = os.path.join(self.plugins_dir, name)
for file in os.listdir(plugin_path):
path = f"plugins/{name}/{file}"
if os.path.isfile(path) and path.endswith(".lua"):
self.lua_dirs.add(name)
self.log.debug(f"py_folders {py_folders}, lua_dirs {self.lua_dirs}")
for name in self.lua_dirs:
# noinspection PyArgumentList
lua = LuaRuntime(encoding="CP1251", source_encoding=config.enc, unpack_returned_tuples=True)
lua_globals = lua.globals()
lua_globals.printRaw = lua.globals().print
lua_globals.exit = lambda x: self.log.info(f"KuiToi: You can't disable server..")
mp = MP(name, lua)
lua_globals.MP = mp
lua_globals.print = mp._print
lua_globals.Util = Util(name, lua)
lua_globals.FS = FS(name, lua)
pa = os.path.abspath(self.plugins_dir)
p0 = os.path.join(pa, name, "?.lua")
p1 = os.path.join(pa, name, "lua", "?.lua")
lua_globals.package.path += f';{p0};{p1}'
with open("modules/PluginsLoader/add_in.lua", "r") as f:
lua.execute(f.read())
self.lua_plugins.update({name: {"lua": lua, "ok": False}})
plugin_path = os.path.join(self.plugins_dir, name)
for file in os.listdir(plugin_path):
path = f"plugins/{name}/{file}"
if os.path.isfile(path) and path.endswith(".lua"):
try:
lua_globals.loadfile(path)()
except Exception as e:
self.loaded_str += f"{name}:no, "
self.log.error(f"Cannot load lua plugin from `{path}`: {e}")
try:
lua_globals.MP.loaded = True
lua_globals.MP.TriggerLocalEvent("onInit")
lua_globals.onInit()
self.lua_plugins[name]['ok'] = True
self.loaded_str += f"{name}:ok, "
except Exception as e:
self.loaded_str += f"{name}:no, "
self.log.error(f"Exception onInit from `{name}`: {e}")
self.log.exception(e)
def unload(self, _):
self.log.debug("Unloading lua plugins")
for name, data in self.lua_plugins.items():
if data['ok']:
self.log.info(i18n.plugins_lua_unload.format(name))
for _, timer in data['lua'].globals().MP._event_timers.items():
timer.stop()

View File

@@ -1,52 +0,0 @@
import os
import types
from core import get_logger
class KuiToi:
def __init__(self, name=None):
if name is None:
raise Exception("BeamMP: Name is required")
self.log = get_logger(f"PluginsLoader | {name}")
self.name = name
def set_name(self, name):
self.name = name
@staticmethod
def register_event(event_name, event_func):
ev.register_event(event_name, event_func)
@staticmethod
def call_event(event_name, *data):
ev.call_event(event_name, *data)
class PluginsLoader:
def __init__(self, plugins_dir):
self.__plugins = {}
self.__plugins_dir = plugins_dir
self.log = get_logger("PluginsLoader")
def load_plugins(self):
self.log.debug("Loading plugins...")
files = os.listdir(self.__plugins_dir)
for file in files:
if file.endswith(".py"):
try:
self.log.debug(f"Loading plugin: {file}")
plugin = types.ModuleType('plugin')
plugin.KuiToi = KuiToi
plugin.print = print
file = os.path.join(self.__plugins_dir, file)
with open(f'{file}', 'r') as f:
code = f.read().replace("import KuiToi\n", "")
exec(code, plugin.__dict__)
plugin.load()
self.__plugins.update({file[:-3]: plugin})
self.log.debug(f"Plugin loaded: {file}")
except Exception as e:
self.log.error(f"Error loading plugin: {file}; Error: {e}")

View File

@@ -0,0 +1,126 @@
import asyncio
import textwrap
from collections import defaultdict, deque
from datetime import datetime, timedelta
from core import utils
class RateLimiter:
def __init__(self, max_calls: int, period: float, ban_time: float):
self.log = utils.get_logger("RateLimiter")
self.max_calls = max_calls
self.period = timedelta(seconds=period)
self.ban_time = timedelta(seconds=ban_time)
self._calls = defaultdict(deque)
self._banned_until = defaultdict(lambda: datetime.min)
self._notified = {}
def parse_console(self, x):
help_msg = textwrap.dedent("""\
RateLimiter menu:
info - list banned ip's
ban - put ip in banlist
unban - force remove ip from banlist
help - print that message""")
_banned_ips = [i for i in self._banned_until if self.is_banned(i, False)]
if len(x) > 0:
match x[0]:
case "info":
self.log.info(f"Trigger {self.max_calls}req/{self.period}. IP will be banned for {self.ban_time}.")
if len(_banned_ips) == 0:
return "No one ip in banlist."
else:
_msg = f"Banned ip{'' if len(_banned_ips) == 1 else 's'}: "
for ip in _banned_ips:
_msg += f"{ip}; "
return _msg
case "unban":
if len(x) == 2:
ip = x[1]
if ip in _banned_ips:
self._notified[ip] = False
self._calls[ip].clear()
self._banned_until[ip] = datetime.now()
return f"{ip} removed from banlist."
return f"{ip} not banned."
else:
return 'rl unban <IP>'
case "ban":
if len(x) == 3:
ip = x[1]
sec = x[2]
if not sec.isdigit():
return f"{sec!r} is not digit."
self._notified[ip] = False
self._calls[ip].clear()
self._banned_until[ip] = datetime.now() + timedelta(seconds=int(sec))
return f"{ip} banned until {self._banned_until[ip]}"
else:
return 'rl ban <IP> <sec>'
case _:
return help_msg
else:
return help_msg
async def notify(self, ip, writer):
if not self._notified[ip]:
self._notified[ip] = True
self.log.warning(f"{ip} banned until {self._banned_until[ip]}.")
try:
writer.write(b'\x0b\x00\x00\x00Eip banned.')
await writer.drain()
except Exception:
pass
def is_banned(self, ip: str, _add_call=True) -> bool:
now = datetime.now()
if now < self._banned_until[ip]:
return True
if _add_call:
self._calls[ip].append(now)
while self._calls[ip] and self._calls[ip][0] + self.period < now:
self._calls[ip].popleft()
if len(self._calls[ip]) > self.max_calls:
self._banned_until[ip] = now + self.ban_time
self._calls[ip].clear()
return True
self._notified[ip] = False
return False
async def handle_request(ip: str, rate_limiter: RateLimiter):
if rate_limiter.is_banned(ip):
print(f"Request from {ip} is banned at {datetime.now()}")
rate_limiter.parse_console(["info"])
async def server_simulation():
rate_limiter = RateLimiter(max_calls=5, period=10, ban_time=30)
# Симулируем несколько запросов от разных IP-адресов
tasks = [
handle_request("192.168.1.1", rate_limiter),
handle_request("192.168.1.2", rate_limiter),
handle_request("192.168.1.1", rate_limiter),
handle_request("192.168.1.1", rate_limiter),
handle_request("192.168.1.3", rate_limiter),
handle_request("192.168.1.2", rate_limiter),
handle_request("192.168.1.1", rate_limiter),
handle_request("192.168.1.2", rate_limiter),
handle_request("192.168.1.3", rate_limiter),
handle_request("192.168.1.1", rate_limiter),
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
handle_request("192.168.1.1", rate_limiter), # This request should trigger a ban
]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(server_simulation())

View File

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

View File

@@ -13,3 +13,4 @@ from .EventsSystem import EventsSystem
from .PluginsLoader import PluginsLoader
from .WebAPISystem import web_app
from .WebAPISystem import _stop as stop_web
from .RateLimiter import RateLimiter

View File

@@ -1,9 +1,160 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.i18n.__init__.py
# File modules.i18n
# Written by: SantaSpeen
# Version 1.0
# Version 1.3
# Licence: FPA
# (c) kuitoi.su 2023
from .i18n import MultiLanguage
import builtins
import json
import os
from json import JSONDecodeError
import requests
from core.utils import get_logger
class i18n:
data = {}
def __init__(self, data):
i18n.data = data
def __getattribute__(self, key):
return i18n.data[key]
class MultiLanguage:
def __init__(self, language: str = None, files_dir="translates/", encoding=None):
if encoding is None:
encoding = config.enc
if language is None:
language = "en"
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_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.__encoding = encoding
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.log = get_logger("i18n")
self.fi = False
self.lang_url = "https://raw.githubusercontent.com/KuiToi/KuiToi-Server/Stable/src/translates/"
def set_language(self, language):
if self.__i18n and language == self.language:
return
self.log.debug(f"set_language({language})")
self.language = language
self.open_file()
self.__i18n = i18n(self.__data)
def open_file(self):
self.log.debug("open_file")
file = self.files_dir + self.language + ".json"
try:
with open(file, encoding=self.__encoding) as f:
self.__data.update(json.load(f))
return
except JSONDecodeError:
self.log.error(
f"Localisation \"{file}\" have JsonDecodeError. Using default localisation: en.")
except FileNotFoundError:
r = requests.get(f"{self.lang_url}{self.language}.json")
if r.status_code != 404:
self.log.info(f"Downloaded new localisation: {self.language}")
loc = r.json()
with open(file, "w", encoding=self.__encoding) as f:
json.dump(loc, f)
self.__data.update(loc)
return
else:
self.log.warning(f"Localisation \"{file}\" not found; Using default localisation: en.")
self.set_language("en")
def builtins_hook(self) -> None:
self.log.debug("used builtins_hook")
builtins.i18n = self.__i18n
builtins.i18n_data = self.__data

View File

@@ -1,48 +0,0 @@
{
"": "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: {}",
"": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
"": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.",
"man_for": "Manual for command",
"man_message_not_found": "man: Manual message not found.",
"man_command_not_found": "man: command \"{}\" not found!",
"": "Command: 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

@@ -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 +0,0 @@
class i18n:
# Basic phases
hello: str = data["hello"]
config_path: str = data["config_path"]
init_ok: str = data["init_ok"]
start: str = data["start"]
stop: str = data["stop"]
# Server auth
auth_need_key: str = data["auth_need_key"]
auth_empty_key: str = data["auth_empty_key"]
auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
auth_use_link: str = data["auth_use_link"]
# GUI phases
GUI_yes: str = data["GUI_yes"]
GUI_no: str = data["GUI_no"]
GUI_ok: str = data["GUI_ok"]
GUI_cancel: str = data["GUI_cancel"]
GUI_need_key_message: str = data["GUI_need_key_message"]
GUI_enter_key_message: str = data["GUI_enter_key_message"]
GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
# Web phases
web_start: str = data["web_start"]
# Command: man
man_message_man: str = data["man_message_man"]
help_message_man: str = data["help_message_man"]
man_for: str = data["man_for"]
man_message_not_found: str = data["man_message_not_found"]
man_command_not_found: str = data["man_command_not_found"]
# Command: help
man_message_help: str = data["man_message_help"]
help_message_help: str = data["help_message_help"]
help_command: str = data["help_command"]
help_message: str = data["help_message"]
help_message_not_found: str = data["help_message_not_found"]
# Command: stop
man_message_stop: str = data["man_message_stop"]
help_message_stop: str = data["help_message_stop"]
# Command: exit
man_message_exit: str = data["man_message_exit"]
help_message_exit: str = data["help_message_exit"]
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

@@ -1,158 +0,0 @@
# -*- coding: utf-8 -*-
# Developed by KuiToi Dev
# File modules.i18n.i18n.py
# Written by: SantaSpeen
# Version 1.0
# Licence: FPA
# (c) kuitoi.su 2023
import builtins
import json
from json import JSONDecodeError
from core.utils import get_logger
class i18n:
def __init__(self, data):
# Basic phases
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
self.auth_need_key: str = data["auth_need_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:
def __init__(self, language: str = None, files_dir="modules/i18n/files/", encoding="utf-8"):
if language is None:
language = "en"
self.__data = {}
self.__i18n = None
self.__encoding = encoding
self.language = language
self.files_dir = files_dir
self.log = get_logger("i18n")
self.set_language(language)
def set_language(self, language):
if language is None:
language = "en"
self.log.debug(f"set_language({language})")
self.language = language
if language != "en":
self.open_file()
else:
# noinspection PyDictDuplicateKeys
self.__data = {
"": "Basic phases",
"hello": "Hello from KuiToi-Server!",
"config_path": "Use {} for config.",
"init_ok": "Initializing ready.",
"start": "Server started!",
"stop": "Goodbye!",
"": "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: {}",
"": "Web phases",
"web_start": "WebAPI running on {} (Press CTRL+C to quit)",
"": "Command: man",
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
"help_message_man": "Display the manual page for COMMAND.",
"man_for": "Manual for command",
"man_message_not_found": "man: Manual message not found.",
"man_command_not_found": "man: command \"{}\" not found!",
"": "Command: 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."
}
self.__i18n = i18n(self.__data)
def open_file(self):
self.log.debug("open_file")
file = self.files_dir + self.language + ".json"
try:
with open(file, encoding=self.__encoding) as f:
self.__data.update(json.load(f))
return
except JSONDecodeError:
self.log.error(
f"Localisation \"{self.language}.json\" have JsonDecodeError. Using default localisation: en.")
except FileNotFoundError:
self.log.warning(f"Localisation \"{self.language}.json\" not found; Using default localisation: en.")
self.set_language("en")
def builtins_hook(self) -> None:
self.log.debug("used builtins_hook")
builtins.i18n = self.__i18n
builtins.i18n_data = self.__data

112
src/modules/i18n/readme.md Normal file
View File

@@ -0,0 +1,112 @@
### Builtins
```python
class i18n:
# Basic phases
hello: str
config_path: str
init_ok: str
start: str
stop: str
# Server auth
auth_need_key: str
auth_empty_key: str
auth_cannot_open_browser: str
auth_use_link: str
# GUI phases
GUI_yes: str
GUI_no: str
GUI_ok: str
GUI_cancel: str
GUI_need_key_message: str
GUI_enter_key_message: str
GUI_cannot_open_browser: str
# Web phases
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_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
man_message_man: str
help_message_man: str
man_for: str
man_message_not_found: str
man_command_not_found: str
# Command: help
man_message_help: str
help_message_help: str
help_command: str
help_message: str
help_message_not_found: str
# Command: stop
man_message_stop: str
help_message_stop: str
# Command: exit
man_message_exit: str
help_message_exit: str
```

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

@@ -0,0 +1,106 @@
{
"": "基本阶段",
"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_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": "停止服务器。"
}

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

@@ -0,0 +1,106 @@
{
"": "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_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."
}

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

@@ -0,0 +1,106 @@
{
"": "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_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": "Выключает сервер."
}

69
win/auto.json Normal file
View File

@@ -0,0 +1,69 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "../src/main.py"
},
{
"optionDest": "onefile",
"value": true
},
{
"optionDest": "console",
"value": true
},
{
"optionDest": "icon_file",
"value": "./icon.ico"
},
{
"optionDest": "name",
"value": "KuiToi-Server"
},
{
"optionDest": "clean_build",
"value": false
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "version_file",
"value": "./metadata.txt"
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}

BIN
win/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

44
win/metadata.txt Normal file
View File

@@ -0,0 +1,44 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0. Must always contain 4 elements.
filevers=(0,4,5,0),
prodvers=(0,4,5,0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x40004,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'040904B0',
[StringStruct(u'CompanyName', u'KuiToi'),
StringStruct(u'FileDescription', u'KuiToi Server'),
StringStruct(u'FileVersion', u'0.4.5.0'),
StringStruct(u'InternalName', u'KuiToi Server'),
StringStruct(u'LegalCopyright', u'© Maxim Khomutov'),
StringStruct(u'OriginalFilename', u'KuiToi-Server.exe'),
StringStruct(u'ProductName', u'KuiToi Server'),
StringStruct(u'ProductVersion', u'0.4.5.0')])
]),
VarFileInfo([VarStruct(u'Translation', [1049, 1251, 1033, 1251, 2052, 950])])
]
)