mirror of
https://github.com/kuitoi/kuitoi-Server.git
synced 2025-08-17 16:25:36 +00:00
Compare commits
12 Commits
6d01f0ef8d
...
7466f987ac
Author | SHA1 | Date | |
---|---|---|---|
7466f987ac | |||
d29bb9de98 | |||
189bd0cc30 | |||
53dae25fcf | |||
e241a7da4c | |||
13be12a7a1 | |||
e348ffecc3 | |||
336aa31732 | |||
d8c667ff51 | |||
658f9ed9c6 | |||
c1cb8dcdba | |||
f52c73ab76 |
@ -26,7 +26,7 @@ I didn't like writing plugins in Lua after using Python; it was very inconvenien
|
|||||||
- [x] Chat
|
- [x] Chat
|
||||||
- [x] Players online counter
|
- [x] Players online counter
|
||||||
- [x] Packets handled (Recursive finding second packet)
|
- [x] Packets handled (Recursive finding second packet)
|
||||||
- [ ] Client events
|
- [x] Client events
|
||||||
- [x] Car synchronizations:
|
- [x] Car synchronizations:
|
||||||
- [x] State packets
|
- [x] State packets
|
||||||
- [x] Spawn cars
|
- [x] Spawn cars
|
||||||
@ -53,7 +53,7 @@ I didn't like writing plugins in Lua after using Python; it was very inconvenien
|
|||||||
- [x] Create custom events
|
- [x] Create custom events
|
||||||
- [x] Return from events
|
- [x] Return from events
|
||||||
- [x] Async support
|
- [x] Async support
|
||||||
- [ ] Add all events
|
- [x] Add all events
|
||||||
- [x] MultiLanguage: (i18n support)
|
- [x] MultiLanguage: (i18n support)
|
||||||
- [ ] Core
|
- [ ] Core
|
||||||
- [x] Console
|
- [x] Console
|
||||||
|
@ -8,3 +8,4 @@ pydantic~=2.0.2
|
|||||||
click~=8.1.4
|
click~=8.1.4
|
||||||
lupa~=2.0
|
lupa~=2.0
|
||||||
toml~=0.10.2
|
toml~=0.10.2
|
||||||
|
colorama~=0.4.6
|
@ -111,8 +111,7 @@ class Client:
|
|||||||
if not self.__alive:
|
if not self.__alive:
|
||||||
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
|
self.log.debug(f"{self.nick}.kick('{reason}') skipped: Not alive;")
|
||||||
return
|
return
|
||||||
# TODO: i18n
|
self.log.info(i18n.client_kicked.format(reason))
|
||||||
self.log.info(f"Kicked with reason: \"{reason}\"")
|
|
||||||
await self._send(f"K{reason}")
|
await self._send(f"K{reason}")
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
|
|
||||||
@ -272,8 +271,7 @@ class Client:
|
|||||||
data = await self._recv(True)
|
data = await self._recv(True)
|
||||||
if data.startswith(b"f"):
|
if data.startswith(b"f"):
|
||||||
file = data[1:].decode(config.enc)
|
file = data[1:].decode(config.enc)
|
||||||
# TODO: i18n
|
self.log.info(i18n.client_mod_request.format(repr(file)))
|
||||||
self.log.info(f"Requested mode: {file!r}")
|
|
||||||
size = -1
|
size = -1
|
||||||
for mod in self.__Core.mods_list:
|
for mod in self.__Core.mods_list:
|
||||||
if type(mod) == int:
|
if type(mod) == int:
|
||||||
@ -312,10 +310,9 @@ class Client:
|
|||||||
tr = time.monotonic() - t
|
tr = time.monotonic() - t
|
||||||
if self.__Core.lock_upload:
|
if self.__Core.lock_upload:
|
||||||
self.__Core.lock_upload = False
|
self.__Core.lock_upload = False
|
||||||
# TODO: i18n
|
msg = i18n.client_mod_sent.format(round(size / MB, 3), math.ceil(size / tr / MB), int(tr))
|
||||||
msg = f"Mod sent: Size {round(size / MB, 3)}mb Speed {math.ceil(size / tr / MB)}Mb/s ({int(tr)}s)"
|
|
||||||
if speed:
|
if speed:
|
||||||
msg += f" of limit {int(speed * 2)}Mb/s"
|
msg += i18n.client_mod_sent_limit.format(int(speed * 2))
|
||||||
self.log.info(msg)
|
self.log.info(msg)
|
||||||
sent = sl0 + sl1
|
sent = sl0 + sl1
|
||||||
ok = sent == size
|
ok = sent == size
|
||||||
@ -323,8 +320,7 @@ class Client:
|
|||||||
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
self.log.debug(f"SplitLoad_0: {sl0}; SplitLoad_1: {sl1}; At all ({ok}): Sent: {sent}; Lost: {lost}")
|
||||||
if not ok:
|
if not ok:
|
||||||
self.__alive = False
|
self.__alive = False
|
||||||
# TODO: i18n
|
self.log.error(i18n.client_mod_sent_error.format(repr(file)))
|
||||||
self.log.error(f"Error while sending: {file!r}")
|
|
||||||
return
|
return
|
||||||
elif data.startswith(b"SR"):
|
elif data.startswith(b"SR"):
|
||||||
path_list = ''
|
path_list = ''
|
||||||
@ -385,10 +381,11 @@ class Client:
|
|||||||
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
|
lua_data = ev.call_lua_event("onVehicleSpawn", self.cid, car_id, car_data[car_data.find("{"):])
|
||||||
if 1 in lua_data:
|
if 1 in lua_data:
|
||||||
allow = False
|
allow = False
|
||||||
ev_data_list = ev.call_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
|
ev_data_list = ev.call_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
|
||||||
d2 = await ev.call_async_event("onCarSpawn", car=car_json, car_id=car_id, player=self)
|
d2 = await ev.call_async_event("onCarSpawn", data=car_json, car_id=car_id, player=self)
|
||||||
ev_data_list.extend(d2)
|
ev_data_list.extend(d2)
|
||||||
for ev_data in ev_data_list:
|
for ev_data in ev_data_list:
|
||||||
|
self.log.debug(ev_data)
|
||||||
# TODO: handle event onCarSpawn
|
# TODO: handle event onCarSpawn
|
||||||
pass
|
pass
|
||||||
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
pkt = f"Os:{self.roles}:{self.nick}:{self.cid}-{car_id}:{car_data}"
|
||||||
@ -435,10 +432,11 @@ class Client:
|
|||||||
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
|
ev.call_lua_event("onVehicleDeleted", self.cid, car_id)
|
||||||
|
|
||||||
admin_allow = False # Delete from admin, for example...
|
admin_allow = False # Delete from admin, for example...
|
||||||
ev_data_list = ev.call_event("onCarDelete", car=self._cars[car_id], car_id=car_id, player=self)
|
ev_data_list = ev.call_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
|
||||||
d2 = await ev.call_async_event("onCarDelete", car=self._cars[car_id], car_id=car_id, player=self)
|
d2 = await ev.call_async_event("onCarDelete", data=self._cars[car_id], car_id=car_id, player=self)
|
||||||
ev_data_list.extend(d2)
|
ev_data_list.extend(d2)
|
||||||
for ev_data in ev_data_list:
|
for ev_data in ev_data_list:
|
||||||
|
self.log.debug(ev_data)
|
||||||
# TODO: handle event onCarDelete
|
# TODO: handle event onCarDelete
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -474,10 +472,11 @@ class Client:
|
|||||||
lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):])
|
lua_data = ev.call_lua_event("onVehicleEdited", self.cid, car_id, data[data.find("{"):])
|
||||||
if 1 in lua_data:
|
if 1 in lua_data:
|
||||||
allow = False
|
allow = False
|
||||||
ev_data_list = ev.call_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
|
ev_data_list = ev.call_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
|
||||||
d2 = await ev.call_async_event("onCarEdited", car=new_car_json, car_id=car_id, player=self)
|
d2 = await ev.call_async_event("onCarEdited", data=new_car_json, car_id=car_id, player=self)
|
||||||
ev_data_list.extend(d2)
|
ev_data_list.extend(d2)
|
||||||
for ev_data in ev_data_list:
|
for ev_data in ev_data_list:
|
||||||
|
self.log.debug(ev_data)
|
||||||
# TODO: handle event onCarEdited
|
# TODO: handle event onCarEdited
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -508,8 +507,8 @@ class Client:
|
|||||||
car_json = json.loads(raw_data[raw_data.find("{"):])
|
car_json = json.loads(raw_data[raw_data.find("{"):])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}")
|
self.log.debug(f"Invalid new_car_json: Error: {e}; Data: {raw_data}")
|
||||||
ev.call_event("onCarReset", car=car_json, car_id=car_id, player=self)
|
ev.call_event("onCarReset", data=car_json, car_id=car_id, player=self)
|
||||||
await ev.call_async_event("onCarReset", car=car_json, car_id=car_id, player=self)
|
await ev.call_async_event("onCarReset", data=car_json, car_id=car_id, player=self)
|
||||||
self.log.debug(f"Car reset: car_id={car_id}")
|
self.log.debug(f"Car reset: car_id={car_id}")
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Invalid car: car_id={car_id}")
|
self.log.debug(f"Invalid car: car_id={car_id}")
|
||||||
@ -539,6 +538,11 @@ class Client:
|
|||||||
|
|
||||||
case "t": # Broken details
|
case "t": # Broken details
|
||||||
self.log.debug(f"Something changed/broken: {raw_data}")
|
self.log.debug(f"Something changed/broken: {raw_data}")
|
||||||
|
cid, car_id = self._get_cid_vid(raw_data[5:])
|
||||||
|
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||||
|
data = raw_data[raw_data.find("{"):]
|
||||||
|
ev.call_event("onCarChanged", car_id=car_id, data=data)
|
||||||
|
await ev.call_async_event("onCarChanged", car_id=car_id, data=data)
|
||||||
await self._send(raw_data, to_all=True, to_self=False)
|
await self._send(raw_data, to_all=True, to_self=False)
|
||||||
|
|
||||||
case "m": # Move focus car
|
case "m": # Move focus car
|
||||||
@ -546,16 +550,18 @@ class Client:
|
|||||||
cid, car_id = self._get_cid_vid(raw_data[5:])
|
cid, car_id = self._get_cid_vid(raw_data[5:])
|
||||||
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
if car_id != -1 and cid == self.cid and self._cars[car_id]:
|
||||||
self._focus_car = car_id
|
self._focus_car = car_id
|
||||||
|
data = raw_data[raw_data.find("{"):]
|
||||||
|
ev.call_event("onCarFocusMove", car_id=car_id, data=data)
|
||||||
|
await ev.call_async_event("onCarFocusMove", car_id=car_id, data=data)
|
||||||
await self._send(raw_data, to_all=True, to_self=True)
|
await self._send(raw_data, to_all=True, to_self=True)
|
||||||
|
|
||||||
async def _connected_handler(self):
|
async def _connected_handler(self):
|
||||||
self.log.info(f"Syncing time: {round(time.monotonic() - self._connect_time, 2)}s")
|
|
||||||
# Client connected
|
# Client connected
|
||||||
ev.call_event("onPlayerJoin", player=self)
|
ev.call_event("onPlayerJoin", player=self)
|
||||||
await ev.call_async_event("onPlayerJoin", player=self)
|
await ev.call_async_event("onPlayerJoin", player=self)
|
||||||
|
|
||||||
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
|
await self._send(f"Sn{self.nick}", to_all=True) # I don't know for what it
|
||||||
await self._send(f"JWelcome {self.nick}!", to_all=True) # Hello message
|
await self._send(i18n.game_welcome_message.format(self.nick), to_all=True) # Hello message
|
||||||
|
|
||||||
for client in self.__Core.clients:
|
for client in self.__Core.clients:
|
||||||
if not client:
|
if not client:
|
||||||
@ -565,6 +571,7 @@ class Client:
|
|||||||
continue
|
continue
|
||||||
await self._send(car['packet'])
|
await self._send(car['packet'])
|
||||||
|
|
||||||
|
self.log.info(i18n.client_sync_time.format(round(time.monotonic() - self._connect_time, 2)))
|
||||||
self._ready = True
|
self._ready = True
|
||||||
|
|
||||||
async def _chat_handler(self, data):
|
async def _chat_handler(self, data):
|
||||||
@ -604,7 +611,7 @@ class Client:
|
|||||||
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
await self._send(f"C:{message}", to_all=to_all, to_self=to_self, writer=writer)
|
||||||
need_send = False
|
need_send = False
|
||||||
except KeyError | AttributeError:
|
except KeyError | AttributeError:
|
||||||
self.log.error(f"Returns invalid data: {ev_data}")
|
self.log.error(i18n.client_event_invalid_data.format(ev_data))
|
||||||
if need_send:
|
if need_send:
|
||||||
if config.Options['log_chat']:
|
if config.Options['log_chat']:
|
||||||
self.log.info(f"{self.nick}: {msg}")
|
self.log.info(f"{self.nick}: {msg}")
|
||||||
@ -620,11 +627,13 @@ class Client:
|
|||||||
await self._send(data, to_all=True, to_self=False)
|
await self._send(data, to_all=True, to_self=False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_bytes = False
|
||||||
try:
|
try:
|
||||||
data = data.decode()
|
data = data.decode()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
|
_bytes = True
|
||||||
self.log.error(f"UnicodeDecodeError: {data}")
|
self.log.error(f"UnicodeDecodeError: {data}")
|
||||||
return
|
self.log.info("Some things are skipping...")
|
||||||
|
|
||||||
# Codes: p, Z in udp_server.py
|
# Codes: p, Z in udp_server.py
|
||||||
match data[0]: # At data[0] code
|
match data[0]: # At data[0] code
|
||||||
@ -632,15 +641,31 @@ class Client:
|
|||||||
await self._connected_handler()
|
await self._connected_handler()
|
||||||
|
|
||||||
case "C": # Chat handler
|
case "C": # Chat handler
|
||||||
|
if _bytes:
|
||||||
|
return
|
||||||
await self._chat_handler(data)
|
await self._chat_handler(data)
|
||||||
|
|
||||||
case "O": # Cars handler
|
case "O": # Cars handler
|
||||||
|
if _bytes:
|
||||||
|
return
|
||||||
await self._handle_car_codes(data)
|
await self._handle_car_codes(data)
|
||||||
|
|
||||||
case "E": # Client events handler
|
case "E": # Client events handler
|
||||||
# TODO: Handle events from client
|
if len(data) < 2:
|
||||||
pass
|
self.log.debug("Tried to send an empty event, ignoring.")
|
||||||
|
return
|
||||||
|
if _bytes:
|
||||||
|
sep = data.find(b":", 2)
|
||||||
|
self.log.warning("Bytes event!")
|
||||||
|
else:
|
||||||
|
sep = data.find(":", 2)
|
||||||
|
if sep == -1:
|
||||||
|
self.log.error(f"Received event in invalid format (missing ':'), got: {data}")
|
||||||
|
event_name = data[2:sep]
|
||||||
|
even_data = data[sep + 1:]
|
||||||
|
ev.call_lua_event(event_name, even_data)
|
||||||
|
ev.call_event(event_name, data=even_data, player=self)
|
||||||
|
await ev.call_async_event(event_name, data=even_data, player=self)
|
||||||
case "N":
|
case "N":
|
||||||
await self._send(data, to_all=True, to_self=False)
|
await self._send(data, to_all=True, to_self=False)
|
||||||
|
|
||||||
@ -681,8 +706,11 @@ class Client:
|
|||||||
ev.call_event("onPlayerDisconnect", player=self)
|
ev.call_event("onPlayerDisconnect", player=self)
|
||||||
await ev.call_async_event("onPlayerDisconnect", player=self)
|
await ev.call_async_event("onPlayerDisconnect", player=self)
|
||||||
|
|
||||||
# TODO: i18n
|
self.log.info(
|
||||||
self.log.info(f"Disconnected, online time: {round((time.monotonic() - self._connect_time) / 60, 2)}min.")
|
i18n.client_player_disconnected.format(
|
||||||
|
round((time.monotonic() - self._connect_time) / 60, 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
self.__Core.clients[self.cid] = None
|
self.__Core.clients[self.cid] = None
|
||||||
del self.__Core.clients_by_id[self.cid]
|
del self.__Core.clients_by_id[self.cid]
|
||||||
del self.__Core.clients_by_nick[self.nick]
|
del self.__Core.clients_by_nick[self.nick]
|
||||||
|
@ -35,7 +35,7 @@ class Client:
|
|||||||
self._ready = False
|
self._ready = False
|
||||||
self._focus_car = -1
|
self._focus_car = -1
|
||||||
self._identifiers = []
|
self._identifiers = []
|
||||||
self._cars: List[Optional[Dict[str, int]]] = []
|
self._cars: List[Union[Dict[str, Union[str, bool, Dict[str, Union[str, List[int], float]]]], None]] = []
|
||||||
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
self._snowman: Dict[str, Union[int, str]] = {"id": -1, "packet": ""}
|
||||||
self._last_position = {}
|
self._last_position = {}
|
||||||
async def __gracefully_kick(self): ...
|
async def __gracefully_kick(self): ...
|
||||||
|
@ -142,8 +142,7 @@ class Core:
|
|||||||
async def heartbeat(self, test=False):
|
async def heartbeat(self, test=False):
|
||||||
if config.Auth["private"] or self.direct:
|
if config.Auth["private"] or self.direct:
|
||||||
if test:
|
if test:
|
||||||
# TODO: i18n
|
self.log.info(i18n.core_direct_mode)
|
||||||
self.log.info(f"Server runnig in Direct connect mode.")
|
|
||||||
self.direct = True
|
self.direct = True
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -184,31 +183,30 @@ class Core:
|
|||||||
if not (body.get("status") is not None and
|
if not (body.get("status") is not None and
|
||||||
body.get("code") is not None and
|
body.get("code") is not None and
|
||||||
body.get("msg") is not None):
|
body.get("msg") is not None):
|
||||||
self.log.error("Missing/invalid json members in backend response")
|
self.log.error(i18n.core_auth_server_error)
|
||||||
raise KeyboardInterrupt
|
return
|
||||||
|
|
||||||
status = body.get("status")
|
status = body.get("status")
|
||||||
msg = body.get("msg")
|
msg = body.get("msg")
|
||||||
if status == "2000":
|
if status == "2000":
|
||||||
if test:
|
if test:
|
||||||
# TODO: i18n
|
self.log.debug(f"Authenticated! {msg}")
|
||||||
self.log.info(f"Authenticated! {msg}")
|
|
||||||
elif status == "200":
|
elif status == "200":
|
||||||
if test:
|
if test:
|
||||||
self.log.info(f"Resumed authenticated session. {msg}")
|
self.log.debug(f"Resumed authenticated session. {msg}")
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Auth: data {data}")
|
self.log.debug(f"Auth: data {data}")
|
||||||
self.log.debug(f"Auth: code {code}, body {body}")
|
self.log.debug(f"Auth: code {code}, body {body}")
|
||||||
self.log.error(f"Backend REFUSED the auth key. Reason: "
|
|
||||||
f"{msg or 'Backend did not provide a reason'}")
|
self.log.error(i18n.core_auth_server_refused.format(
|
||||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
msg or i18n.core_auth_server_refused_no_reason))
|
||||||
|
self.log.info(i18n.core_auth_server_refused_direct_node)
|
||||||
self.direct = True
|
self.direct = True
|
||||||
else:
|
else:
|
||||||
self.direct = True
|
self.direct = True
|
||||||
if test:
|
if test:
|
||||||
# TODO: i18n
|
self.log.error(i18n.core_auth_server_no_response)
|
||||||
self.log.error("Cannot authenticate server.")
|
self.log.info(i18n.core_auth_server_refused_direct_node)
|
||||||
self.log.info(f"Server still runnig, but only in Direct connect mode.")
|
|
||||||
# if not config.Auth['private']:
|
# if not config.Auth['private']:
|
||||||
# raise KeyboardInterrupt
|
# raise KeyboardInterrupt
|
||||||
|
|
||||||
@ -263,8 +261,7 @@ class Core:
|
|||||||
self.log.debug(f"mods_list: {self.mods_list}")
|
self.log.debug(f"mods_list: {self.mods_list}")
|
||||||
len_mods = len(self.mods_list) - 1
|
len_mods = len(self.mods_list) - 1
|
||||||
if len_mods > 0:
|
if len_mods > 0:
|
||||||
# TODO: i18n
|
self.log.info(i18n.core_mods_loaded.format(len_mods, round(self.mods_list[0] / MB, 2)))
|
||||||
self.log.info(f"Loaded {len_mods} mods: {round(self.mods_list[0] / MB, 2)}mb")
|
|
||||||
self.log.info(i18n.init_ok)
|
self.log.info(i18n.init_ok)
|
||||||
|
|
||||||
await self.heartbeat(True)
|
await self.heartbeat(True)
|
||||||
@ -278,7 +275,6 @@ class Core:
|
|||||||
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
|
||||||
|
|
||||||
await ev.call_async_event("_plugins_start")
|
await ev.call_async_event("_plugins_start")
|
||||||
# await ev.call_async_event("_lua_plugins_start")
|
|
||||||
|
|
||||||
self.run = True
|
self.run = True
|
||||||
self.log.info(i18n.start)
|
self.log.info(i18n.start)
|
||||||
|
@ -24,13 +24,11 @@ class TCPServer:
|
|||||||
|
|
||||||
async def auth_client(self, reader, writer):
|
async def auth_client(self, reader, writer):
|
||||||
client = self.Core.create_client(reader, writer)
|
client = self.Core.create_client(reader, writer)
|
||||||
# TODO: i18n
|
self.log.info(i18n.core_identifying_connection)
|
||||||
self.log.info(f"Identifying new ClientConnection...")
|
|
||||||
data = await client._recv(True)
|
data = await client._recv(True)
|
||||||
self.log.debug(f"Version: {data}")
|
self.log.debug(f"Version: {data}")
|
||||||
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
|
if data.decode("utf-8") != f"VC{self.Core.client_major_version}":
|
||||||
# TODO: i18n
|
await client.kick(i18n.core_player_kick_outdated)
|
||||||
await client.kick("Outdated Version.")
|
|
||||||
return False, client
|
return False, client
|
||||||
else:
|
else:
|
||||||
await client._send(b"S") # Accepted client version
|
await client._send(b"S") # Accepted client version
|
||||||
@ -38,8 +36,7 @@ class TCPServer:
|
|||||||
data = await client._recv(True)
|
data = await client._recv(True)
|
||||||
self.log.debug(f"Key: {data}")
|
self.log.debug(f"Key: {data}")
|
||||||
if len(data) > 50:
|
if len(data) > 50:
|
||||||
# TODO: i18n
|
await client.kick(i18n.core_player_kick_bad_key)
|
||||||
await client.kick("Invalid Key (too long)!")
|
|
||||||
return False, client
|
return False, client
|
||||||
client._key = data.decode("utf-8")
|
client._key = data.decode("utf-8")
|
||||||
ev.call_event("onPlayerSentKey", player=client)
|
ev.call_event("onPlayerSentKey", player=client)
|
||||||
@ -50,8 +47,7 @@ class TCPServer:
|
|||||||
res = await response.json()
|
res = await response.json()
|
||||||
self.log.debug(f"res: {res}")
|
self.log.debug(f"res: {res}")
|
||||||
if res.get("error"):
|
if res.get("error"):
|
||||||
# TODO: i18n
|
await client.kick(i18n.core_player_kick_invalid_key)
|
||||||
await client.kick('Invalid key! Please restart your game.')
|
|
||||||
return False, client
|
return False, client
|
||||||
client.nick = res["username"]
|
client.nick = res["username"]
|
||||||
client.roles = res["roles"]
|
client.roles = res["roles"]
|
||||||
@ -60,21 +56,20 @@ class TCPServer:
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
client._update_logger()
|
client._update_logger()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: i18n
|
|
||||||
self.log.error(f"Auth error: {e}")
|
self.log.error(f"Auth error: {e}")
|
||||||
await client.kick('Invalid authentication data! Try to reconnect in 5 minutes.')
|
await client.kick(i18n.core_player_kick_auth_server_fail)
|
||||||
return False, client
|
return False, client
|
||||||
|
|
||||||
for _client in self.Core.clients:
|
for _client in self.Core.clients:
|
||||||
if not _client:
|
if not _client:
|
||||||
continue
|
continue
|
||||||
if _client.nick == client.nick and _client.guest == client.guest:
|
if _client.nick == client.nick and _client.guest == client.guest:
|
||||||
# TODO: i18n
|
await _client.kick(i18n.core_player_kick_stale)
|
||||||
await client.kick('Stale Client (replaced by new client)')
|
|
||||||
return False, client
|
client.log.info(i18n.core_player_set_id.format(client.pid))
|
||||||
|
|
||||||
allow = True
|
allow = True
|
||||||
reason = "You are not allowed on the server!"
|
reason = i18n.core_player_kick_no_allowed_default_reason
|
||||||
|
|
||||||
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
|
lua_data = ev.call_lua_event("onPlayerAuth", client.nick, client.roles, client.guest, client.identifiers)
|
||||||
for data in lua_data:
|
for data in lua_data:
|
||||||
@ -90,12 +85,10 @@ class TCPServer:
|
|||||||
ev.call_event("onPlayerAuthenticated", player=client)
|
ev.call_event("onPlayerAuthenticated", player=client)
|
||||||
|
|
||||||
if len(self.Core.clients_by_id) > config.Game["players"]:
|
if len(self.Core.clients_by_id) > config.Game["players"]:
|
||||||
# TODO: i18n
|
await client.kick(i18n.core_player_kick_server_full)
|
||||||
await client.kick("Server full!")
|
|
||||||
return False, client
|
return False, client
|
||||||
else:
|
else:
|
||||||
# TODO: i18n
|
self.log.info(i18n.core_identifying_okay)
|
||||||
self.log.info("Identification success")
|
|
||||||
await self.Core.insert_client(client)
|
await self.Core.insert_client(client)
|
||||||
|
|
||||||
return True, client
|
return True, client
|
||||||
@ -127,8 +120,8 @@ class TCPServer:
|
|||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
case _:
|
case _:
|
||||||
# TODO: i18n
|
|
||||||
self.log.error(f"Unknown code: {code}")
|
self.log.error(f"Unknown code: {code}")
|
||||||
|
self.log.info("Report about that!")
|
||||||
writer.close()
|
writer.close()
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
@ -148,7 +141,6 @@ class TCPServer:
|
|||||||
del cl
|
del cl
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: i18n
|
|
||||||
self.log.error("Error while handling connection...")
|
self.log.error("Error while handling connection...")
|
||||||
self.log.exception(e)
|
self.log.exception(e)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -165,8 +157,7 @@ class TCPServer:
|
|||||||
async with server:
|
async with server:
|
||||||
await server.serve_forever()
|
await server.serve_forever()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# TODO: i18n
|
self.log.error(i18n.core_bind_failed.format(e))
|
||||||
self.log.error("Cannot bind port")
|
|
||||||
raise e
|
raise e
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
@ -14,6 +14,7 @@ from prompt_toolkit import PromptSession, print_formatted_text, HTML
|
|||||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||||
from prompt_toolkit.completion import NestedCompleter
|
from prompt_toolkit.completion import NestedCompleter
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
|
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
|
||||||
from prompt_toolkit.patch_stdout import patch_stdout
|
from prompt_toolkit.patch_stdout import patch_stdout
|
||||||
|
|
||||||
from core import get_logger
|
from core import get_logger
|
||||||
@ -28,6 +29,7 @@ class Console:
|
|||||||
debug=False) -> None:
|
debug=False) -> None:
|
||||||
self.__logger = get_logger("console")
|
self.__logger = get_logger("console")
|
||||||
self.__is_run = False
|
self.__is_run = False
|
||||||
|
self.no_cmd = False
|
||||||
self.__prompt_in = prompt_in
|
self.__prompt_in = prompt_in
|
||||||
self.__prompt_out = prompt_out
|
self.__prompt_out = prompt_out
|
||||||
self.__not_found = not_found
|
self.__not_found = not_found
|
||||||
@ -128,10 +130,18 @@ class Console:
|
|||||||
return self.__alias.copy()
|
return self.__alias.copy()
|
||||||
|
|
||||||
def _write(self, t):
|
def _write(self, t):
|
||||||
if t.startswith("html:"):
|
if self.no_cmd:
|
||||||
print_formatted_text(HTML(t[5:]))
|
print(t)
|
||||||
else:
|
return
|
||||||
print_formatted_text(t)
|
try:
|
||||||
|
if t.startswith("html:"):
|
||||||
|
print_formatted_text(HTML(t[5:]))
|
||||||
|
else:
|
||||||
|
print_formatted_text(t)
|
||||||
|
except NoConsoleScreenBufferError:
|
||||||
|
print("Works in non cmd mode.")
|
||||||
|
self.no_cmd = True
|
||||||
|
print(t)
|
||||||
|
|
||||||
def write(self, s: AnyStr):
|
def write(self, s: AnyStr):
|
||||||
if isinstance(s, (list, tuple)):
|
if isinstance(s, (list, tuple)):
|
||||||
@ -202,11 +212,18 @@ class Console:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
with patch_stdout():
|
with patch_stdout():
|
||||||
cmd_in = await session.prompt_async(
|
if self.no_cmd:
|
||||||
self.__prompt_in,
|
cmd_in = input(self.__prompt_in)
|
||||||
completer=self.completer,
|
else:
|
||||||
auto_suggest=AutoSuggestFromHistory()
|
try:
|
||||||
)
|
cmd_in = await session.prompt_async(
|
||||||
|
self.__prompt_in,
|
||||||
|
completer=self.completer,
|
||||||
|
auto_suggest=AutoSuggestFromHistory()
|
||||||
|
)
|
||||||
|
except NoConsoleScreenBufferError:
|
||||||
|
print("Works in non cmd mode.")
|
||||||
|
self.no_cmd = True
|
||||||
cmd_s = cmd_in.split(" ")
|
cmd_s = cmd_in.split(" ")
|
||||||
cmd = cmd_s[0]
|
cmd = cmd_s[0]
|
||||||
if cmd == "":
|
if cmd == "":
|
||||||
|
@ -22,19 +22,21 @@ class EventsSystem:
|
|||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
self.as_tasks = []
|
self.as_tasks = []
|
||||||
self.__events = {
|
self.__events = {
|
||||||
"onServerStarted": [],
|
"onServerStarted": [], # No handler
|
||||||
"onPlayerSentKey": [], # Only sync
|
"onPlayerSentKey": [], # Only sync, no handler
|
||||||
"onPlayerAuthenticated": [], # Only sync
|
"onPlayerAuthenticated": [], # (!) Only sync, With handler
|
||||||
"onPlayerJoin": [],
|
"onPlayerJoin": [], # (!) With handler
|
||||||
"onChatReceive": [],
|
"onChatReceive": [], # (!) With handler
|
||||||
"onCarSpawn": [],
|
"onCarSpawn": [], # (!) With handler
|
||||||
"onCarDelete": [],
|
"onCarDelete": [], # (!) With handler (admin allow)
|
||||||
"onCarEdited": [],
|
"onCarEdited": [], # (!) With handler
|
||||||
"onCarReset": [],
|
"onCarReset": [], # No handler
|
||||||
"onSentPing": [], # Only sync
|
"onCarChanged": [], # No handler
|
||||||
"onChangePosition": [], # Only sync
|
"onCarFocusMove": [], # No handler
|
||||||
"onPlayerDisconnect": [],
|
"onSentPing": [], # Only sync, no handler
|
||||||
"onServerStopped": [],
|
"onChangePosition": [], # Only sync, no handler
|
||||||
|
"onPlayerDisconnect": [], # No handler
|
||||||
|
"onServerStopped": [], # No handler
|
||||||
}
|
}
|
||||||
self.__async_events = {
|
self.__async_events = {
|
||||||
"onServerStarted": [],
|
"onServerStarted": [],
|
||||||
@ -44,6 +46,8 @@ class EventsSystem:
|
|||||||
"onCarDelete": [],
|
"onCarDelete": [],
|
||||||
"onCarEdited": [],
|
"onCarEdited": [],
|
||||||
"onCarReset": [],
|
"onCarReset": [],
|
||||||
|
"onCarChanged": [],
|
||||||
|
"onCarFocusMove": [],
|
||||||
"onPlayerDisconnect": [],
|
"onPlayerDisconnect": [],
|
||||||
"onServerStopped": []
|
"onServerStopped": []
|
||||||
}
|
}
|
||||||
@ -62,6 +66,7 @@ class EventsSystem:
|
|||||||
"onVehicleDeleted": [], # onCarDelete
|
"onVehicleDeleted": [], # onCarDelete
|
||||||
"onVehicleReset": [], # onCarReset
|
"onVehicleReset": [], # onCarReset
|
||||||
"onFileChanged": [], # TODO lua onFileChanged
|
"onFileChanged": [], # TODO lua onFileChanged
|
||||||
|
"onConsoleInput": [], # kt.add_command
|
||||||
}
|
}
|
||||||
|
|
||||||
def builtins_hook(self):
|
def builtins_hook(self):
|
||||||
|
@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -63,6 +64,12 @@ class MP:
|
|||||||
def _print(self, *args):
|
def _print(self, *args):
|
||||||
args = list(args)
|
args = list(args)
|
||||||
for i, arg in enumerate(args):
|
for i, arg in enumerate(args):
|
||||||
|
if isinstance(arg, str):
|
||||||
|
try:
|
||||||
|
text = arg.encode("CP1251").decode(config.enc).replace("\u001b", "\x1b")
|
||||||
|
args[i] = re.sub(r'\x1b\[.*?m', '', text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
pass
|
||||||
if "LuaTable" in str(type(arg)):
|
if "LuaTable" in str(type(arg)):
|
||||||
args[i] = self._lua.globals().Util.JsonEncode(arg)
|
args[i] = self._lua.globals().Util.JsonEncode(arg)
|
||||||
s = " ".join(map(str, args))
|
s = " ".join(map(str, args))
|
||||||
|
@ -91,6 +91,10 @@ class KuiToi:
|
|||||||
return False
|
return False
|
||||||
return bool(self.get_player(cid=pid, nick=nick))
|
return bool(self.get_player(cid=pid, nick=nick))
|
||||||
|
|
||||||
|
def add_command(self, key, func, man, desc, custom_completer) -> dict:
|
||||||
|
self.log.debug("Requests add_command")
|
||||||
|
return console.add_command(key, func, man, desc, custom_completer)
|
||||||
|
|
||||||
|
|
||||||
class PluginsLoader:
|
class PluginsLoader:
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"": "基础阶段",
|
"": "基本阶段",
|
||||||
"hello": "来自KuiToi服务器的问候!",
|
"hello": "来自KuiToi-Server的问候!",
|
||||||
"config_path": "使用{}进行配置。",
|
"config_path": "使用{}进行配置。",
|
||||||
"init_ok": "初始化完成。",
|
"init_ok": "初始化完成。",
|
||||||
"start": "服务器已启动!",
|
"start": "服务器已启动!",
|
||||||
"stop": "服务器已停止!",
|
"stop": "服务器已停止!",
|
||||||
|
|
||||||
"": "服务器认证",
|
"": "服务器认证",
|
||||||
"auth_need_key": "需要BeamMP密钥才能启动!",
|
"auth_need_key": "需要BeamMP密钥才能运行!",
|
||||||
"auth_empty_key": "BeamMP密钥为空!",
|
"auth_empty_key": "BeamMP密钥为空!",
|
||||||
"auth_cannot_open_browser": "无法打开浏览器:{}",
|
"auth_cannot_open_browser": "无法打开浏览器:{}",
|
||||||
"auth_use_link": "使用此链接:{}",
|
"auth_use_link": "使用此链接:{}",
|
||||||
@ -17,32 +17,65 @@
|
|||||||
"GUI_no": "否",
|
"GUI_no": "否",
|
||||||
"GUI_ok": "确定",
|
"GUI_ok": "确定",
|
||||||
"GUI_cancel": "取消",
|
"GUI_cancel": "取消",
|
||||||
"GUI_need_key_message": "需要BeamMP密钥才能启动!\n是否在浏览器中打开链接以获取密钥?",
|
"GUI_need_key_message": "需要BeamMP密钥才能运行!\n您是否要在浏览器中打开链接获取密钥?",
|
||||||
"GUI_enter_key_message": "请输入密钥:",
|
"GUI_enter_key_message": "请输入密钥:",
|
||||||
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
"GUI_cannot_open_browser": "无法打开浏览器。\n请使用此链接:{}",
|
||||||
|
|
||||||
"": "Web阶段",
|
"": "Web阶段",
|
||||||
"web_start": "WebAPI已启动{}(CTRL+C停止)",
|
"web_start": "WebAPI已启动于{}(按CTRL+C停止)",
|
||||||
|
|
||||||
|
"": "核心短语",
|
||||||
|
"core_bind_failed": "无法绑定端口。错误:{}",
|
||||||
|
"core_direct_mode": "服务器以直接连接模式启动。",
|
||||||
|
"core_auth_server_error": "从BeamMP认证服务器接收到无效响应。",
|
||||||
|
"core_auth_server_refused": "BeamMP认证服务器拒绝了您的密钥。原因:{}",
|
||||||
|
"core_auth_server_refused_no_reason": "BeamMP认证服务器没有提供原因。",
|
||||||
|
"core_auth_server_refused_direct_node": "服务器仍在运行,但以直接连接模式运行。",
|
||||||
|
"core_auth_server_no_response": "无法验证服务器。",
|
||||||
|
"core_mods_loaded": "已加载{}个模组。{}Mb",
|
||||||
|
"core_identifying_connection": "正在处理新连接...",
|
||||||
|
"core_player_kick_outdated": "BeamMP版本不正确。",
|
||||||
|
"core_player_kick_bad_key": "传递的密钥无效!",
|
||||||
|
"core_player_kick_invalid_key": "无效的密钥!请重新启动游戏。",
|
||||||
|
"core_player_kick_auth_server_fail": "BeamMP认证服务器失败!请在5分钟后再次尝试连接。",
|
||||||
|
"core_player_kick_stale": "过时的客户端。(由新连接替换)",
|
||||||
|
"core_player_kick_no_allowed_default_reason": "您不受欢迎。拒绝访问。",
|
||||||
|
"core_player_kick_server_full": "服务器已满。",
|
||||||
|
"core_player_set_id": "玩家设置ID {}",
|
||||||
|
"core_identifying_okay": "成功登录。",
|
||||||
|
|
||||||
|
"": "游戏内短语",
|
||||||
|
"game_welcome_message": "欢迎{}!",
|
||||||
|
|
||||||
|
"": "客户端类短语",
|
||||||
|
"client_mod_request": "请求模组:{}",
|
||||||
|
"client_mod_sent": "已发送模组:大小:{}mb,速度:{}Mb/s({}秒)",
|
||||||
|
"client_mod_sent_limit": "(限制{}Mb/s)",
|
||||||
|
"client_mod_sent_error": "发送模组时出错:{}",
|
||||||
|
"client_sync_time": "同步时间{}秒。",
|
||||||
|
"client_kicked": "因\"{}\"原因被踢出。",
|
||||||
|
"client_event_invalid_data": "从事件返回的数据无效:{}",
|
||||||
|
"client_player_disconnected": "离开服务器。游戏时间:{}分钟。",
|
||||||
|
|
||||||
"": "命令:man",
|
"": "命令:man",
|
||||||
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
"man_message_man": "man - 显示COMMAND的帮助页面。\n用法:man COMMAND",
|
||||||
"help_message_man": "显示COMMAND的帮助页面。",
|
"help_message_man": "显示COMMAND的帮助页面。",
|
||||||
"man_for": "帮助页面",
|
"man_for": "帮助页面",
|
||||||
"man_message_not_found": "man:找不到帮助页面。",
|
"man_message_not_found": "man:找不到帮助页面。",
|
||||||
"man_command_not_found": "man:找不到\"{}\"命令!",
|
"man_command_not_found": "man:找不到命令\"{}\"!",
|
||||||
|
|
||||||
"": "命令:help",
|
"": "命令:help",
|
||||||
"man_message_help": "help - 显示命令的名称和简短描述。\n用法:help [--raw]\n命令`help`列出所有可用的命令,并为每个命令提供简短描述。",
|
"man_message_help": "help - 显示命令的名称和简要说明。\n用法:help [--raw]\n`help`命令显示所有可用命令的名称和简要说明。",
|
||||||
"help_message_help": "显示命令的名称和简短描述。",
|
"help_message_help": "显示命令的名称和简要说明。",
|
||||||
"help_command": "命令",
|
"help_command": "命令",
|
||||||
"help_message": "文本",
|
"help_message": "文本",
|
||||||
"help_message_not_found": "无文本",
|
"help_message_not_found": "未找到文本。",
|
||||||
|
|
||||||
"": "命令:stop",
|
"": "命令:stop",
|
||||||
"man_message_stop": "stop - 关闭服务器。\n用法:stop",
|
"man_message_stop": "stop - 停止服务器。\n用法:stop",
|
||||||
"help_message_stop": "关闭服务器。",
|
"help_message_stop": "停止服务器。",
|
||||||
|
|
||||||
"": "命令:exit",
|
"": "命令:exit",
|
||||||
"man_message_exit": "exit - 关闭服务器。\n用法:exit",
|
"man_message_exit": "exit - 停止服务器。\n用法:exit",
|
||||||
"help_message_exit": "关闭服务器。"
|
"help_message_exit": "停止服务器。"
|
||||||
}
|
}
|
@ -1,42 +1,75 @@
|
|||||||
{
|
{
|
||||||
"": "Basic phases",
|
"": "Basic phases",
|
||||||
"hello": "Greetings from KuiToi Server!",
|
"hello": "Hello from KuiToi-Server!",
|
||||||
"config_path": "Use {} to configure.",
|
"config_path": "Use {} to configure.",
|
||||||
"init_ok": "Initialization complete.",
|
"init_ok": "Initialization completed.",
|
||||||
"start": "Server started!",
|
"start": "Server started!",
|
||||||
"stop": "Server stopped!",
|
"stop": "Server stopped!",
|
||||||
|
|
||||||
"": "Server auth",
|
"": "Server auth",
|
||||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
"auth_need_key": "BeamMP key is required to run!",
|
||||||
"auth_empty_key": "The BeamMP key is empty!",
|
"auth_empty_key": "BeamMP key is empty!",
|
||||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||||
"auth_use_link": "Use this link: {}",
|
"auth_use_link": "Use this link: {}",
|
||||||
|
|
||||||
"": "GUI phases",
|
"": "GUI phases",
|
||||||
"GUI_yes": "Yes",
|
"GUI_yes": "Yes",
|
||||||
"GUI_no": "No",
|
"GUI_no": "No",
|
||||||
"GUI_ok": "Ok",
|
"GUI_ok": "OK",
|
||||||
"GUI_cancel": "Cancel",
|
"GUI_cancel": "Cancel",
|
||||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
|
"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_enter_key_message": "Please enter the key:",
|
||||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||||
|
|
||||||
"": "Web phases",
|
"": "Web phases",
|
||||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
"web_start": "WebAPI started on {} (CTRL+C to stop)",
|
||||||
|
|
||||||
|
"": "Core phrases",
|
||||||
|
"core_bind_failed": "Failed to bind port. Error: {}",
|
||||||
|
"core_direct_mode": "Server started in direct connection mode.",
|
||||||
|
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
|
||||||
|
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
|
||||||
|
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
|
||||||
|
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
|
||||||
|
"core_auth_server_no_response": "Failed to authenticate the server.",
|
||||||
|
"core_mods_loaded": "Loaded {} mods. {}Mb",
|
||||||
|
"core_identifying_connection": "Processing new connection...",
|
||||||
|
"core_player_kick_outdated": "Incorrect version of BeamMP.",
|
||||||
|
"core_player_kick_bad_key": "Invalid key passed!",
|
||||||
|
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
|
||||||
|
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
|
||||||
|
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
||||||
|
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
||||||
|
"core_player_kick_server_full": "Server is full.",
|
||||||
|
"core_player_set_id": "Player set ID {}",
|
||||||
|
"core_identifying_okay": "Successful login.",
|
||||||
|
|
||||||
|
"": "In-game phrases",
|
||||||
|
"game_welcome_message": "Welcome {}!",
|
||||||
|
|
||||||
|
"": "Client class phrases",
|
||||||
|
"client_mod_request": "Requested mod: {}",
|
||||||
|
"client_mod_sent": "Mod sent: Size: {}mb, Speed: {}Mb/s ({}sec)",
|
||||||
|
"client_mod_sent_limit": " (limit {}Mb/s)",
|
||||||
|
"client_mod_sent_error": "Error sending mod: {}",
|
||||||
|
"client_sync_time": "Sync time {}sec.",
|
||||||
|
"client_kicked": "Kicked for reason: \"{}\"",
|
||||||
|
"client_event_invalid_data": "Invalid data returned from event: {}",
|
||||||
|
"client_player_disconnected": "Left the server. Playtime: {} min",
|
||||||
|
|
||||||
"": "Command: man",
|
"": "Command: man",
|
||||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
||||||
"help_message_man": "Displays help page for COMMAND.",
|
"help_message_man": "Shows the help page for COMMAND.",
|
||||||
"man_for": "Help page for",
|
"man_for": "Help page for",
|
||||||
"man_message_not_found": "man: Help page not found.",
|
"man_message_not_found": "man: Help page not found.",
|
||||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
"man_command_not_found": "man: Command \"{}\" not found!",
|
||||||
|
|
||||||
"": "Command: help",
|
"": "Command: help",
|
||||||
"man_message_help": "help - 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.",
|
"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": "Displays the names and short descriptions of commands.",
|
"help_message_help": "Shows the names and brief descriptions of commands",
|
||||||
"help_command": "Command",
|
"help_command": "Command",
|
||||||
"help_message": "Description",
|
"help_message": "Text",
|
||||||
"help_message_not_found": "No description available.",
|
"help_message_not_found": "No text found",
|
||||||
|
|
||||||
"": "Command: stop",
|
"": "Command: stop",
|
||||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
||||||
|
@ -24,6 +24,39 @@
|
|||||||
"": "Web phases",
|
"": "Web phases",
|
||||||
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
|
"web_start": "WebAPI запустился на {} (CTRL+C для выключения)",
|
||||||
|
|
||||||
|
"": "Core phrases",
|
||||||
|
"core_bind_failed": "Не получилось занять порт. Ошибка: {}",
|
||||||
|
"core_direct_mode": "Сервер запушен в режиме прямого подключения.",
|
||||||
|
"core_auth_server_error": "Поступил не корректный ответ от сервером авторизации BeamMP.",
|
||||||
|
"core_auth_server_refused": "Сервер авторизации BeamMP отклонил ваш ключ. Причина: {}",
|
||||||
|
"core_auth_server_refused_no_reason": "Сервер авторизации BeamMP не сообщил причины.",
|
||||||
|
"core_auth_server_refused_direct_node": "Сервер всё ещё работает, но в режиме прямого подключения.",
|
||||||
|
"core_auth_server_no_response": "Не получилось авторизовать сервер.",
|
||||||
|
"core_mods_loaded": "Загружено {} модов. {}Мб",
|
||||||
|
"core_identifying_connection": "Обработка нового подключения...",
|
||||||
|
"core_player_kick_outdated": "Не подходящая версия BeamMP.",
|
||||||
|
"core_player_kick_bad_key": "Передан не правильный ключ!",
|
||||||
|
"core_player_kick_invalid_key": "Неверный ключ! Пожалуйста, перезапустите свою игру.",
|
||||||
|
"core_player_kick_auth_server_fail": "Сбой сервера аутентификации! Попробуйте снова подключиться через 5 минут.",
|
||||||
|
"core_player_kick_stale": "Устаревший клиент. (Заменено новым подключением)",
|
||||||
|
"core_player_kick_no_allowed_default_reason": "Вам не рады на этом сервере. Вход запрещён.",
|
||||||
|
"core_player_kick_server_full": "Сервер полон.",
|
||||||
|
"core_player_set_id": "Игрок получил ID {}",
|
||||||
|
"core_identifying_okay": "Успешный вход.",
|
||||||
|
|
||||||
|
"": "In-game phrases",
|
||||||
|
"game_welcome_message": "Добро пожаловать {}!",
|
||||||
|
|
||||||
|
"": "Client class phrases",
|
||||||
|
"client_mod_request": "Запрошен мод: {}",
|
||||||
|
"client_mod_sent": "Мод отправлен: Вес: {}мб, Скорость: {}Мб/с ({}сек)",
|
||||||
|
"client_mod_sent_limit": " (лимит {}Мб/с)",
|
||||||
|
"client_mod_sent_error": "Ошибка при отправке мода: {}",
|
||||||
|
"client_sync_time": "Время синхронизации {}сек.",
|
||||||
|
"client_kicked": "Кикнут по причине: \"{}\"",
|
||||||
|
"client_event_invalid_data": "Из ивента вернулись не верные данные: {}",
|
||||||
|
"client_player_disconnected": "Вышел с сервера. Время игры: {} мин",
|
||||||
|
|
||||||
"": "Command: man",
|
"": "Command: man",
|
||||||
"man_message_man": "man - Показывает страничку помощи для COMMAND.\nИспользование: man COMMAND",
|
"man_message_man": "man - Показывает страничку помощи для COMMAND.\nИспользование: man COMMAND",
|
||||||
"help_message_man": "Показывает страничку помощи для COMMAND.",
|
"help_message_man": "Показывает страничку помощи для COMMAND.",
|
||||||
|
@ -1,93 +1,83 @@
|
|||||||
class i18n:
|
class i18n:
|
||||||
# Basic phases
|
# Basic phases
|
||||||
hello: str = data["hello"]
|
hello: str
|
||||||
config_path: str = data["config_path"]
|
config_path: str
|
||||||
init_ok: str = data["init_ok"]
|
init_ok: str
|
||||||
start: str = data["start"]
|
start: str
|
||||||
stop: str = data["stop"]
|
stop: str
|
||||||
|
|
||||||
# Server auth
|
# Server auth
|
||||||
auth_need_key: str = data["auth_need_key"]
|
auth_need_key: str
|
||||||
auth_empty_key: str = data["auth_empty_key"]
|
auth_empty_key: str
|
||||||
auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
|
auth_cannot_open_browser: str
|
||||||
auth_use_link: str = data["auth_use_link"]
|
auth_use_link: str
|
||||||
|
|
||||||
# GUI phases
|
# GUI phases
|
||||||
GUI_yes: str = data["GUI_yes"]
|
GUI_yes: str
|
||||||
GUI_no: str = data["GUI_no"]
|
GUI_no: str
|
||||||
GUI_ok: str = data["GUI_ok"]
|
GUI_ok: str
|
||||||
GUI_cancel: str = data["GUI_cancel"]
|
GUI_cancel: str
|
||||||
GUI_need_key_message: str = data["GUI_need_key_message"]
|
GUI_need_key_message: str
|
||||||
GUI_enter_key_message: str = data["GUI_enter_key_message"]
|
GUI_enter_key_message: str
|
||||||
GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
|
GUI_cannot_open_browser: str
|
||||||
|
|
||||||
# Web phases
|
# Web phases
|
||||||
web_start: str = data["web_start"]
|
web_start: str
|
||||||
|
|
||||||
|
# Core phrases
|
||||||
|
|
||||||
|
core_bind_failed: str
|
||||||
|
core_direct_mode: str
|
||||||
|
core_auth_server_error: str
|
||||||
|
core_auth_server_refused: str
|
||||||
|
core_auth_server_refused_no_reason: str
|
||||||
|
core_auth_server_refused_direct_node: str
|
||||||
|
core_auth_server_no_response: str
|
||||||
|
core_mods_loaded: str
|
||||||
|
core_identifying_connection: str
|
||||||
|
core_player_kick_outdated: str
|
||||||
|
core_player_kick_bad_key: str
|
||||||
|
core_player_kick_invalid_key: str
|
||||||
|
core_player_kick_auth_server_fail: str
|
||||||
|
core_player_kick_stale: str
|
||||||
|
core_player_kick_no_allowed_default_reason: str
|
||||||
|
core_player_kick_server_full: str
|
||||||
|
core_player_set_id: str
|
||||||
|
core_identifying_okay: str
|
||||||
|
|
||||||
|
# In-game phrases
|
||||||
|
|
||||||
|
game_welcome_message: str
|
||||||
|
|
||||||
|
# Client class phrases
|
||||||
|
|
||||||
|
client_mod_request: str
|
||||||
|
client_mod_sent: str
|
||||||
|
client_mod_sent_limit: str
|
||||||
|
client_mod_sent_error: str
|
||||||
|
client_sync_time: str
|
||||||
|
client_kicked: str
|
||||||
|
client_event_invalid_data: str
|
||||||
|
client_player_disconnected: str
|
||||||
|
|
||||||
# Command: man
|
# Command: man
|
||||||
man_message_man: str = data["man_message_man"]
|
man_message_man: str
|
||||||
help_message_man: str = data["help_message_man"]
|
help_message_man: str
|
||||||
man_for: str = data["man_for"]
|
man_for: str
|
||||||
man_message_not_found: str = data["man_message_not_found"]
|
man_message_not_found: str
|
||||||
man_command_not_found: str = data["man_command_not_found"]
|
man_command_not_found: str
|
||||||
|
|
||||||
# Command: help
|
# Command: help
|
||||||
man_message_help: str = data["man_message_help"]
|
man_message_help: str
|
||||||
help_message_help: str = data["help_message_help"]
|
help_message_help: str
|
||||||
help_command: str = data["help_command"]
|
help_command: str
|
||||||
help_message: str = data["help_message"]
|
help_message: str
|
||||||
help_message_not_found: str = data["help_message_not_found"]
|
help_message_not_found: str
|
||||||
|
|
||||||
# Command: stop
|
# Command: stop
|
||||||
man_message_stop: str = data["man_message_stop"]
|
man_message_stop: str
|
||||||
help_message_stop: str = data["help_message_stop"]
|
help_message_stop: str
|
||||||
|
|
||||||
# Command: exit
|
# Command: exit
|
||||||
man_message_exit: str = data["man_message_exit"]
|
man_message_exit: str
|
||||||
help_message_exit: str = data["help_message_exit"]
|
help_message_exit: str
|
||||||
|
|
||||||
data = {
|
|
||||||
"": "Basic phases",
|
|
||||||
"hello": "Hello from KuiToi-Server!",
|
|
||||||
"config_path": "Use {} for config.",
|
|
||||||
"init_ok": "Initializing ready.",
|
|
||||||
"start": "Server started!",
|
|
||||||
"stop": "Goodbye!",
|
|
||||||
|
|
||||||
"": "Server auth",
|
|
||||||
"auth_need_key": "BEAM key needed for starting the server!",
|
|
||||||
"auth_empty_key": "Key is empty!",
|
|
||||||
"auth_cannot_open_browser": "Cannot open browser: {}",
|
|
||||||
"auth_use_link": "Use this link: {}",
|
|
||||||
|
|
||||||
"": "GUI phases",
|
|
||||||
"GUI_yes": "Yes",
|
|
||||||
"GUI_no": "No",
|
|
||||||
"GUI_ok": "Ok",
|
|
||||||
"GUI_cancel": "Cancel",
|
|
||||||
"GUI_need_key_message": "BEAM key needed for starting the server!\nDo you need to open the web link to obtain the key?",
|
|
||||||
"GUI_enter_key_message": "Please type your key:",
|
|
||||||
"GUI_cannot_open_browser": "Cannot open browser.\nUse this link: {}",
|
|
||||||
|
|
||||||
"": "Command: man",
|
|
||||||
"man_message_man": "man - display the manual page for COMMAND.\nUsage: man COMMAND",
|
|
||||||
"help_message_man": "Display the manual page for COMMAND.",
|
|
||||||
"man_for": "Manual for command",
|
|
||||||
"man_message_not_found": "man: Manual message not found.",
|
|
||||||
"man_command_not_found": "man: command \"{}\" not found!",
|
|
||||||
|
|
||||||
"": "Command: help",
|
|
||||||
"man_message_help": "help - display names and brief descriptions of available commands.\nUsage: help [--raw]\nThe `help` command displays a list of all available commands along with a brief description of each command.",
|
|
||||||
"help_message_help": "Display names and brief descriptions of available commands",
|
|
||||||
"help_command": "Command",
|
|
||||||
"help_message": "Help message",
|
|
||||||
"help_message_not_found": "No help message found",
|
|
||||||
|
|
||||||
"": "Command: stop",
|
|
||||||
"man_message_stop": "stop - Just shutting down the server.\nUsage: stop",
|
|
||||||
"help_message_stop": "Server shutdown.",
|
|
||||||
|
|
||||||
"": "Command: exit",
|
|
||||||
"man_message_exit": "exit - Just shutting down the server.\nUsage: stop",
|
|
||||||
"help_message_exit": "Server shutdown."
|
|
||||||
}
|
|
||||||
|
@ -14,56 +14,13 @@ from core.utils import get_logger
|
|||||||
|
|
||||||
|
|
||||||
class i18n:
|
class i18n:
|
||||||
|
data = {}
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
# Basic phases
|
i18n.data = data
|
||||||
self.hello: str = data["hello"]
|
|
||||||
self.config_path: str = data["config_path"]
|
|
||||||
self.init_ok: str = data["init_ok"]
|
|
||||||
self.start: str = data["start"]
|
|
||||||
self.stop: str = data["stop"]
|
|
||||||
|
|
||||||
# Server auth
|
def __getattribute__(self, key):
|
||||||
self.auth_need_key: str = data["auth_need_key"]
|
return i18n.data[key]
|
||||||
self.auth_empty_key: str = data["auth_empty_key"]
|
|
||||||
self.auth_cannot_open_browser: str = data["auth_cannot_open_browser"]
|
|
||||||
self.auth_use_link: str = data["auth_use_link"]
|
|
||||||
|
|
||||||
# GUI phases
|
|
||||||
self.GUI_yes: str = data["GUI_yes"]
|
|
||||||
self.GUI_no: str = data["GUI_no"]
|
|
||||||
self.GUI_ok: str = data["GUI_ok"]
|
|
||||||
self.GUI_cancel: str = data["GUI_cancel"]
|
|
||||||
self.GUI_need_key_message: str = data["GUI_need_key_message"]
|
|
||||||
self.GUI_enter_key_message: str = data["GUI_enter_key_message"]
|
|
||||||
self.GUI_cannot_open_browser: str = data["GUI_cannot_open_browser"]
|
|
||||||
|
|
||||||
# Web phases
|
|
||||||
self.web_start: str = data["web_start"]
|
|
||||||
|
|
||||||
# Command: man
|
|
||||||
self.man_message_man: str = data["man_message_man"]
|
|
||||||
self.help_message_man: str = data["help_message_man"]
|
|
||||||
self.man_for: str = data["man_for"]
|
|
||||||
self.man_message_not_found: str = data["man_message_not_found"]
|
|
||||||
self.man_command_not_found: str = data["man_command_not_found"]
|
|
||||||
|
|
||||||
# Command: help
|
|
||||||
self.man_message_help: str = data["man_message_help"]
|
|
||||||
self.help_message_help: str = data["help_message_help"]
|
|
||||||
self.help_command: str = data["help_command"]
|
|
||||||
self.help_message: str = data["help_message"]
|
|
||||||
self.help_message_not_found: str = data["help_message_not_found"]
|
|
||||||
|
|
||||||
# Command: help
|
|
||||||
self.man_message_stop: str = data["man_message_stop"]
|
|
||||||
self.help_message_stop: str = data["help_message_stop"]
|
|
||||||
|
|
||||||
# Command: exit
|
|
||||||
self.man_message_exit: str = data["man_message_exit"]
|
|
||||||
self.help_message_exit: str = data["help_message_exit"]
|
|
||||||
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
|
|
||||||
class MultiLanguage:
|
class MultiLanguage:
|
||||||
@ -91,53 +48,86 @@ class MultiLanguage:
|
|||||||
else:
|
else:
|
||||||
# noinspection PyDictDuplicateKeys
|
# noinspection PyDictDuplicateKeys
|
||||||
self.__data = {
|
self.__data = {
|
||||||
"": "Basic phases",
|
"": "Basic phases",
|
||||||
"hello": "Greetings from KuiToi Server!",
|
"hello": "Hello from KuiToi-Server!",
|
||||||
"config_path": "Use {} to configure.",
|
"config_path": "Use {} to configure.",
|
||||||
"init_ok": "Initialization complete.",
|
"init_ok": "Initialization completed.",
|
||||||
"start": "Server started!",
|
"start": "Server started!",
|
||||||
"stop": "Server stopped!",
|
"stop": "Server stopped!",
|
||||||
|
|
||||||
"": "Server auth",
|
"": "Server auth",
|
||||||
"auth_need_key": "A BeamMP key is required to start the server!",
|
"auth_need_key": "BeamMP key is required to run!",
|
||||||
"auth_empty_key": "The BeamMP key is empty!",
|
"auth_empty_key": "BeamMP key is empty!",
|
||||||
"auth_cannot_open_browser": "Failed to open browser: {}",
|
"auth_cannot_open_browser": "Failed to open browser: {}",
|
||||||
"auth_use_link": "Use this link: {}",
|
"auth_use_link": "Use this link: {}",
|
||||||
|
|
||||||
"": "GUI phases",
|
"": "GUI phases",
|
||||||
"GUI_yes": "Yes",
|
"GUI_yes": "Yes",
|
||||||
"GUI_no": "No",
|
"GUI_no": "No",
|
||||||
"GUI_ok": "Ok",
|
"GUI_ok": "OK",
|
||||||
"GUI_cancel": "Cancel",
|
"GUI_cancel": "Cancel",
|
||||||
"GUI_need_key_message": "A BeamMP key is required to start the server!\nDo you want to open the link in a browser to obtain the key?",
|
"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_enter_key_message": "Please enter the key:",
|
||||||
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
"GUI_cannot_open_browser": "Failed to open browser.\nUse this link: {}",
|
||||||
|
|
||||||
"": "Web phases",
|
"": "Web phases",
|
||||||
"web_start": "WebAPI started at {} (Press CTRL+C to quit)",
|
"web_start": "WebAPI started on {} (CTRL+C to stop)",
|
||||||
|
|
||||||
"": "Command: man",
|
"": "Core phrases",
|
||||||
"man_message_man": "man - Displays help page for COMMAND.\nUsage: man COMMAND",
|
"core_bind_failed": "Failed to bind port. Error: {}",
|
||||||
"help_message_man": "Displays help page for COMMAND.",
|
"core_direct_mode": "Server started in direct connection mode.",
|
||||||
"man_for": "Help page for",
|
"core_auth_server_error": "Received invalid response from BeamMP authentication server.",
|
||||||
"man_message_not_found": "man: Help page not found.",
|
"core_auth_server_refused": "The BeamMP authentication server refused your key. Reason: {}",
|
||||||
"man_command_not_found": "man: Command \"{}\" not found!",
|
"core_auth_server_refused_no_reason": "The BeamMP authentication server did not provide a reason.",
|
||||||
|
"core_auth_server_refused_direct_node": "The server is still running, but in direct connection mode.",
|
||||||
|
"core_auth_server_no_response": "Failed to authenticate the server.",
|
||||||
|
"core_mods_loaded": "Loaded {} mods. {}Mb",
|
||||||
|
"core_identifying_connection": "Processing new connection...",
|
||||||
|
"core_player_kick_outdated": "Incorrect version of BeamMP.",
|
||||||
|
"core_player_kick_bad_key": "Invalid key passed!",
|
||||||
|
"core_player_kick_invalid_key": "Invalid key! Please restart your game.",
|
||||||
|
"core_player_kick_auth_server_fail": "BeamMP authentication server failed! Please try to connect again in 5 minutes.",
|
||||||
|
"core_player_kick_stale": "Stale client. (Replaced by new connection)",
|
||||||
|
"core_player_kick_no_allowed_default_reason": "You are not welcome on this server. Access denied.",
|
||||||
|
"core_player_kick_server_full": "Server is full.",
|
||||||
|
"core_player_set_id": "Player set ID {}",
|
||||||
|
"core_identifying_okay": "Successful login.",
|
||||||
|
|
||||||
"": "Command: help",
|
"": "In-game phrases",
|
||||||
"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.",
|
"game_welcome_message": "Welcome {}!",
|
||||||
"help_message_help": "Displays the names and short descriptions of commands.",
|
|
||||||
"help_command": "Command",
|
|
||||||
"help_message": "Description",
|
|
||||||
"help_message_not_found": "No description available.",
|
|
||||||
|
|
||||||
"": "Command: stop",
|
"": "Client class phrases",
|
||||||
"man_message_stop": "stop - Stops the server.\nUsage: stop",
|
"client_mod_request": "Requested mod: {}",
|
||||||
"help_message_stop": "Stops the server.",
|
"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",
|
||||||
|
|
||||||
"": "Command: exit",
|
"": "Command: man",
|
||||||
"man_message_exit": "exit - Stops the server.\nUsage: exit",
|
"man_message_man": "man - Shows the help page for COMMAND.\nUsage: man COMMAND",
|
||||||
"help_message_exit": "Stops the server."
|
"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."
|
||||||
|
}
|
||||||
self.__i18n = i18n(self.__data)
|
self.__i18n = i18n(self.__data)
|
||||||
|
|
||||||
def open_file(self):
|
def open_file(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user