From 642f8b52d1fab51936f152d6b637dfb41c356046 Mon Sep 17 00:00:00 2001 From: The many faced demon <154847721+themanyfaceddemon@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:48:45 +0300 Subject: [PATCH] Net part 6 (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Описание PR'ра ### Медиа ### Техническая информация - [ ] PR полностью завершён. - [ ] Мне **НЕ** нужна помощь для завершения PR. - [ ] Проверено на локальной машине. - [ ] Документация обновлена. - [ ] Тесты обновлены / добавлены. ### Изменения changes: NOT --- .github/workflows/unittest.yml | 5 +- .gitignore | 7 - .vscode/launch.json | 12 + Code/{systems/audio_system => }/__init__.py | 0 Code/api/__init__.py | 3 + Code/api/chat.py | 15 ++ Code/app_client_main.py | 10 - Code/gui/__init__.py | 1 - Code/gui/fonts_setup.py | 31 +++ Code/gui/login_window.py | 221 ------------------ Code/gui/main_window.py | 18 -- Code/gui/start.py | 85 +++++++ Code/main.py | 16 ++ Code/misc.py | 19 ++ Code/root_path.py | 4 +- Code/systems/audio_system/audio_manager.py | 6 - Code/systems/bin_system/__init__.py | 1 - Code/systems/bin_system/bin_system.py | 76 ------ Code/systems/events_system/__init__.py | 4 - Code/systems/events_system/event_manager.py | 46 ---- Code/systems/events_system/events/__init__.py | 3 - Code/systems/events_system/events/test.py | 10 - Code/systems/events_system/register.py | 46 ---- Code/systems/loc/__init__.py | 1 + Code/systems/loc/loc.py | 133 +++++++++++ Code/systems/misc.py | 16 -- Code/systems/network/__init__.py | 1 - Code/systems/network/client_unit.py | 183 --------------- Code/systems/texture_system/__init__.py | 5 +- Code/systems/texture_system/color.py | 68 ------ Code/systems/texture_system/texture_system.py | 13 +- Content/Client/fronts/Monocraft/COPYRIGHT.txt | 2 + Content/Client/fronts/Monocraft/LICENSE.txt | 91 ++++++++ Content/Client/fronts/Monocraft/Monocraft.otf | Bin 0 -> 22020 bytes Content/Client/loc/eng/main_app.loc | 1 + Content/Client/loc/rus/standarts.loc | 3 + Content/Client/loc/rus/start_app.loc | 14 ++ GUI/windows/AuthDialog.ui | 82 ------- GUI/windows/LoginWindow.ui | 167 ------------- GUI/windows/MainWindow.ui | 31 --- GUI/windows/RegistrationDialog.ui | 86 ------- GUI/windows/ServerContentDownloadDialog.ui | 60 ----- Tests/Texture/TextureSystem.py | 8 +- requirements.txt | 8 +- 44 files changed, 446 insertions(+), 1166 deletions(-) create mode 100644 .vscode/launch.json rename Code/{systems/audio_system => }/__init__.py (100%) create mode 100644 Code/api/__init__.py create mode 100644 Code/api/chat.py delete mode 100644 Code/app_client_main.py create mode 100644 Code/gui/fonts_setup.py delete mode 100644 Code/gui/login_window.py delete mode 100644 Code/gui/main_window.py create mode 100644 Code/gui/start.py create mode 100644 Code/main.py create mode 100644 Code/misc.py delete mode 100644 Code/systems/audio_system/audio_manager.py delete mode 100644 Code/systems/bin_system/__init__.py delete mode 100644 Code/systems/bin_system/bin_system.py delete mode 100644 Code/systems/events_system/__init__.py delete mode 100644 Code/systems/events_system/event_manager.py delete mode 100644 Code/systems/events_system/events/__init__.py delete mode 100644 Code/systems/events_system/events/test.py delete mode 100644 Code/systems/events_system/register.py create mode 100644 Code/systems/loc/__init__.py create mode 100644 Code/systems/loc/loc.py delete mode 100644 Code/systems/misc.py delete mode 100644 Code/systems/network/__init__.py delete mode 100755 Code/systems/network/client_unit.py delete mode 100644 Code/systems/texture_system/color.py create mode 100644 Content/Client/fronts/Monocraft/COPYRIGHT.txt create mode 100644 Content/Client/fronts/Monocraft/LICENSE.txt create mode 100644 Content/Client/fronts/Monocraft/Monocraft.otf create mode 100644 Content/Client/loc/eng/main_app.loc create mode 100644 Content/Client/loc/rus/standarts.loc create mode 100644 Content/Client/loc/rus/start_app.loc delete mode 100644 GUI/windows/AuthDialog.ui delete mode 100644 GUI/windows/LoginWindow.ui delete mode 100644 GUI/windows/MainWindow.ui delete mode 100644 GUI/windows/RegistrationDialog.ui delete mode 100644 GUI/windows/ServerContentDownloadDialog.ui diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 235c701..b3d2307 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -25,14 +25,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - - - name: Set PYTHONPATH - run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)/Code" >> $GITHUB_ENV - name: Run tests timeout-minutes: 10 run: | - python -m unittest discover -s Tests -p "*.py" -v -f + python -m unittest discover -s Tests -p "*.py" -v env: pythonLocation: /opt/hostedtoolcache/Python/3.12.3/x64 LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.3/x64/lib diff --git a/.gitignore b/.gitignore index 3590828..2f241e9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,6 @@ ENV/ env.bak/ venv.bak/ -# VS and VSC -.vs/* -.vscode/* - # Server data Content/Servers/* @@ -26,6 +22,3 @@ Content/Servers/* Content/Compiled/* *_compiled*.gif *_compiled*.png - -# PyQT6 -*_ui.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..00f9c4d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Start client", + "type": "debugpy", + "request": "launch", + "program": "Code/main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/Code/systems/audio_system/__init__.py b/Code/__init__.py similarity index 100% rename from Code/systems/audio_system/__init__.py rename to Code/__init__.py diff --git a/Code/api/__init__.py b/Code/api/__init__.py new file mode 100644 index 0000000..29575e4 --- /dev/null +++ b/Code/api/__init__.py @@ -0,0 +1,3 @@ +from .chat import ChatModule + +__all__ = ['ChatModule'] diff --git a/Code/api/chat.py b/Code/api/chat.py new file mode 100644 index 0000000..b7f3318 --- /dev/null +++ b/Code/api/chat.py @@ -0,0 +1,15 @@ +import logging + +from DMBotNetwork import Client + +logger = logging.getLogger("Chat") + +class ChatModule(Client): + async def net_ooc_chat(self, sender: str, msg: str): + logger.info(f"[OOC] {sender}: {msg}") + return + + @staticmethod + async def send_ooc(msg: str): + await Client.request_method("ooc_chat", msg=msg) + return diff --git a/Code/app_client_main.py b/Code/app_client_main.py deleted file mode 100644 index 2481798..0000000 --- a/Code/app_client_main.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys - -from gui import LoginWindow -from PyQt6.QtWidgets import QApplication - -if __name__ == "__main__": - app = QApplication(sys.argv) - window = LoginWindow() - window.show() - sys.exit(app.exec()) diff --git a/Code/gui/__init__.py b/Code/gui/__init__.py index b0df4df..e69de29 100644 --- a/Code/gui/__init__.py +++ b/Code/gui/__init__.py @@ -1 +0,0 @@ -from gui.login_window import LoginWindow diff --git a/Code/gui/fonts_setup.py b/Code/gui/fonts_setup.py new file mode 100644 index 0000000..12bacb6 --- /dev/null +++ b/Code/gui/fonts_setup.py @@ -0,0 +1,31 @@ +import sys + +import dearpygui.dearpygui as dpg +from root_path import ROOT_PATH + + +class FontManager: + @staticmethod + def load_fonts(): + with dpg.font_registry(): + with dpg.font(str(ROOT_PATH / 'Content' / 'Client' / 'fronts' / 'Monocraft' / 'Monocraft.otf'), 13) as default_font: + dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) + dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) + + dpg.add_font_range(0x0391, 0x03C9) #Greek character range + dpg.add_font_range(0x2070, 0x209F) #Range of upper and lower numerical indices + + if sys.platform == 'win32': # Иди нахуй винда + _remap_chars() + + dpg.bind_font(default_font) + +def _remap_chars(): + biglet = 0x0410 + for i1 in range(0x00C0, 0xE0): + dpg.add_char_remap(i1, biglet) + dpg.add_char_remap(i1 + 0x20, biglet + 0x20) + biglet += 1 + + dpg.add_char_remap(0x00A8, 0x0401) # Ё + dpg.add_char_remap(0x00B8, 0x0451) # ё diff --git a/Code/gui/login_window.py b/Code/gui/login_window.py deleted file mode 100644 index cfa82c2..0000000 --- a/Code/gui/login_window.py +++ /dev/null @@ -1,221 +0,0 @@ -import os - -from gui.main_window import MainApplicationWindow -from PyQt6.QtCore import QThread, pyqtSignal -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import (QDialog, QLabel, QLineEdit, QMainWindow, - QMessageBox, QProgressBar, QPushButton) -from PyQt6.uic import loadUi -from root_path import ROOT_PATH -from systems.misc import GlobalClass -from systems.network import ClientUnit - - -class LoginWindow(QMainWindow, GlobalClass): - def __init__(self): - super().__init__() - - if not hasattr(self, '_initialized'): - self._initialized = True - - loadUi(os.path.join(ROOT_PATH, 'GUI', 'windows', 'LoginWindow.ui'), self) - - # Привязка виджетов - self.ip_line = self.findChild(QLineEdit, 'IPLine') - self.port_line = self.findChild(QLineEdit, 'PortLine') - self.server_check_button = self.findChild(QPushButton, 'ServerCheckButton') - self.server_check_label = self.findChild(QLabel, 'ServerCheckLabel') - - self.login_button = self.findChild(QPushButton, 'LoginButton') - self.register_button = self.findChild(QPushButton, 'RegisterButton') - - self.set_server_check_label("failure") - - # Привязка функций к кнопкам - self.server_check_button.clicked.connect(self.check_server) - self.login_button.clicked.connect(self.open_auth_window) - self.register_button.clicked.connect(self.open_registration_window) - - # Начальное состояние кнопок - self.login_button.setEnabled(False) - self.register_button.setEnabled(False) - - def set_server_check_label(self, state: str): - self.server_check_label.setPixmap(QPixmap(os.path.join(ROOT_PATH, 'GUI', 'images', 'status', f'{state}_64_64.png'))) - - def check_server(self): - ip = self.ip_line.text() - port = self.port_line.text() - - if not ip or not port: - QMessageBox.warning(self, "Ошибка", "Все поля должны быть заполнены!") - self.set_server_check_label("failure") - self.set_buttons(False) - return - - if not self.is_valid_ip(ip): - QMessageBox.warning(self, "Ошибка", "IP неверно указан. Такого IP не может существовать.") - self.set_server_check_label("failure") - self.set_buttons(False) - return - - if not self.is_valid_port(port): - QMessageBox.warning(self, "Ошибка", "Порт неверно указан. Такого порта не может существовать.") - self.set_server_check_label("failure") - self.set_buttons(False) - return - - cl_unit = ClientUnit() - try: - cl_unit.check_server(ip, int(port)) - self.set_server_check_label("success") - self.set_buttons(True) - - except Exception as e: - QMessageBox.warning(self, "Ошибка", f"Невозможно подключиться к серверу: {str(e)}") - self.set_server_check_label("failure") - self.set_buttons(False) - return - - def open_auth_window(self): - auth_window = AuthDialog(self) - auth_window.exec() - - def open_registration_window(self): - registration_window = RegistrationDialog(self) - registration_window.exec() - - def set_buttons(self, value: bool): - self.login_button.setEnabled(value) - self.register_button.setEnabled(value) - - def is_valid_ip(self, ip: str) -> bool: - parts = ip.split('.') - if len(parts) != 4: - return False - - for part in parts: - if not part.isdigit(): - return False - - if not 0 <= int(part) <= 255: - return False - - return True - - def is_valid_port(self, port: str) -> bool: - if not port.isdigit(): - return False - - port_num = int(port) - return 1 <= port_num <= 65535 - - def run_download_server_content(self) -> None: - download_dialog = ServerContentDownloadDialog(self) - download_dialog.exec() - - def run_main_app(self) -> None: - self.close() - self.main_window = MainApplicationWindow() - self.main_window.show() - -class AuthDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - loadUi(os.path.join(ROOT_PATH, 'GUI', 'windows', 'AuthDialog.ui'), self) - - self.username_line = self.findChild(QLineEdit, 'UsernameLine') - self.password_line = self.findChild(QLineEdit, 'PasswordLine') - self.login_button = self.findChild(QPushButton, 'LoginButton') - - self.login_button.clicked.connect(self.authenticate) - - def authenticate(self): - username = self.username_line.text() - password = self.password_line.text() - - if not username or not password: - QMessageBox.warning(self, "Ошибка", "Все поля должны быть заполнены!") - return - - cl_unit = ClientUnit() - try: - cl_unit.login(username, password) - self.close_current_and_open_main_window() - - except Exception as e: - QMessageBox.warning(self, "Ошибка", f"Невозможно выполнить аутентификацию: {str(e)}") - - def close_current_and_open_main_window(self): - self.close() - self.parent().run_download_server_content() - -class RegistrationDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - loadUi(os.path.join(ROOT_PATH, 'GUI', 'windows', 'RegistrationDialog.ui'), self) - - self.username_line = self.findChild(QLineEdit, 'UsernameLine') - self.password_line = self.findChild(QLineEdit, 'PasswordLine') - self.password_line_2 = self.findChild(QLineEdit, 'PasswordLine_2') - self.register_button = self.findChild(QPushButton, 'RegisterButton') - - self.register_button.clicked.connect(self.register) - - def register(self): - username = self.username_line.text() - password = self.password_line.text() - password_2 = self.password_line_2.text() - - if not username or not password or not password_2: - QMessageBox.warning(self, "Ошибка", "Все поля должны быть заполнены!") - return - - if password != password_2: - QMessageBox.warning(self, "Ошибка", "Пароли должны совпадать!") - return - - cl_unit = ClientUnit() - try: - cl_unit.register(username, password) - self.close_current_and_open_main_window() - except Exception as err: - QMessageBox.warning(self, "Ошибка", f"Логин который вы указали уже занят. Подробная ошибка: {err}") - - def close_current_and_open_main_window(self): - self.close() - self.parent().run_download_server_content() - -class DownloadThread(QThread): - progress_updated = pyqtSignal(int, int) - - def __init__(self): - super().__init__() - - def run(self): - cl_unit = ClientUnit() - cl_unit.download_server_content(self.report_progress) - - def report_progress(self, downloaded_size, total_size): - self.progress_updated.emit(downloaded_size, total_size) - -class ServerContentDownloadDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - loadUi(os.path.join(ROOT_PATH, 'GUI', 'windows', 'ServerContentDownloadDialog.ui'), self) - - self.progress_bar = self.findChild(QProgressBar, 'progressBar') - - self.download_thread = DownloadThread() - self.download_thread.progress_updated.connect(self.update_progress) - self.download_thread.start() - - def update_progress(self, downloaded_size, total_size): - self.progress_bar.setMaximum(total_size) - self.progress_bar.setValue(downloaded_size) - - if downloaded_size >= total_size: - self.download_thread.quit() - self.download_thread.wait() - self.close() - self.parent().run_main_app() diff --git a/Code/gui/main_window.py b/Code/gui/main_window.py deleted file mode 100644 index 24a924f..0000000 --- a/Code/gui/main_window.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from PyQt6.QtGui import QPixmap -from PyQt6.QtWidgets import (QDialog, QLabel, QLineEdit, QMainWindow, - QMessageBox, QPushButton) -from PyQt6.uic import loadUi -from root_path import ROOT_PATH -from systems.misc import GlobalClass -from systems.network import ClientUnit - - -class MainApplicationWindow(QMainWindow, GlobalClass): - def __init__(self): - super().__init__() - - if not hasattr(self, '_initialized'): - self._initialized = True - loadUi(os.path.join(ROOT_PATH, 'GUI', 'windows', 'MainWindow.ui'), self) diff --git a/Code/gui/start.py b/Code/gui/start.py new file mode 100644 index 0000000..89f4642 --- /dev/null +++ b/Code/gui/start.py @@ -0,0 +1,85 @@ +from pathlib import Path + +import dearpygui.dearpygui as dpg +from dearpygui_async import DearPyGuiAsync +from DMBotNetwork import Client +from misc import decode_string +from root_path import ROOT_PATH +from systems.loc import Localization as loc +from .fonts_setup import FontManager + + +class DMClientApp: + def __init__(self): + self.dpg_async = DearPyGuiAsync() + + def setup(self): + dpg.create_context() + FontManager.load_fonts() + dpg.create_viewport(title=loc.get_string("main-app-name"), width=600, min_width=600, height=400, min_height=400) + dpg.setup_dearpygui() + self._create_warning_window() + + def _create_warning_window(self): + with dpg.window(label="Warning", tag="warning_window", no_move=True, no_close=True, no_collapse=True, width=380, no_resize=True): + dpg.add_text(loc.get_string("main_text_warning_window"), wrap=380) + dpg.add_button(label=loc.get_string("yes_warning_window"), callback=self._on_yes) + dpg.add_button(label=loc.get_string("no_warning_window"), callback=self._on_no) + + def _on_yes(self, *args): + dpg.delete_item("warning_window") + self._create_connect_window() + + def _on_no(self, *args): + self.stop() + + def _create_connect_window(self): + if dpg.does_item_exist("connect_window"): + dpg.focus_item("connect_window") + return + + with dpg.window(label="Connect", tag="connect_window", no_close=True, no_collapse=True, no_move=True, width=380): + dpg.add_text(loc.get_string("connect_main_text")) + + dpg.add_input_text(hint=loc.get_string("connect_login_hint") , tag="connect_login") + dpg.add_input_text(hint=loc.get_string("connect_password_hint"), tag="connect_password", password=True) + dpg.add_input_text(hint=loc.get_string("connect_host_hint") , tag="connect_host") + dpg.add_input_int (label=loc.get_string("connect_port_lable") , tag="connect_port") + dpg.add_button(label=loc.get_string("connect_button"), callback=self._run_client) + + async def _run_client(self, *args): + await Client.close_connection() + + login_value = decode_string(dpg.get_value("connect_login")) + password_value = decode_string(dpg.get_value("connect_password")) + host_value = decode_string(dpg.get_value("connect_host")) + port_value = dpg.get_value("connect_port") + + Client.set_login(login_value) + Client.set_password(password_value) + Client.set_host(host_value) + Client.set_port(port_value) + + server_path = Path(ROOT_PATH) / "Content" / "Servers" + Client.set_server_file_path(server_path) + + await Client.connect() + + if Client._listen_task is None: # FIXME: Так делать нельзя, но проверить законектились ли мы иначе на текущий момент невозможно. Cain. DM-Bot-net v 0.0.12 + with dpg.window(no_title_bar=True, modal=True, width=380) as con_err: + dpg.add_text(loc.get_string("on_connect_error_msg"), wrap=380) + + dpg.add_button(label=loc.get_string("ok"), callback=lambda: dpg.delete_item(con_err)) + + else: + dpg.delete_item("connect_window") + # TODO: Запрос других окон с сервера. + + def run(self): + dpg.show_viewport() + self.dpg_async.run() + dpg.start_dearpygui() + dpg.destroy_context() + + def stop(self): + dpg.stop_dearpygui() diff --git a/Code/main.py b/Code/main.py new file mode 100644 index 0000000..7e67b8c --- /dev/null +++ b/Code/main.py @@ -0,0 +1,16 @@ +import logging + +from api import * +from DMBotNetwork import Client +from gui.start import DMClientApp +from root_path import ROOT_PATH +from systems.loc import Localization as loc + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + loc.load_translations(ROOT_PATH / "Content" / "Client" / "loc" / 'rus') + + app = DMClientApp() + Client() + app.setup() + app.run() diff --git a/Code/misc.py b/Code/misc.py new file mode 100644 index 0000000..ac7212f --- /dev/null +++ b/Code/misc.py @@ -0,0 +1,19 @@ +import sys + + +def decode_string(instr : str): + """Данную функцию нужно вызывать каждый раз при получении данных от пользователя (input_text). Винда у нас особая и не работает по нормальному. Язык стола зачарований из майна нам не нужен (и тот который блять на винде только) + """ + if not sys.platform == 'win32': # Иди нахуй винда + return instr + + translation_table = str.maketrans({ + chr(i): chr(i + 0x350) for i in range(0x00C0, 0x100) + }) + + translation_table.update({ + 0x00B8: chr(0x0451), + 0x00A8: chr(0x0401) + }) + + return instr.translate(translation_table) diff --git a/Code/root_path.py b/Code/root_path.py index d5fd67f..6704866 100644 --- a/Code/root_path.py +++ b/Code/root_path.py @@ -1,3 +1,3 @@ -import os +from pathlib import Path -ROOT_PATH = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir)) +ROOT_PATH = Path(__file__).parents[1] diff --git a/Code/systems/audio_system/audio_manager.py b/Code/systems/audio_system/audio_manager.py deleted file mode 100644 index 7a30019..0000000 --- a/Code/systems/audio_system/audio_manager.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydub import AudioSegment -from pydub.playback import play - - -class AudioManager: - __slots__ = [] diff --git a/Code/systems/bin_system/__init__.py b/Code/systems/bin_system/__init__.py deleted file mode 100644 index 2bd8183..0000000 --- a/Code/systems/bin_system/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from systems.bin_system.bin_system import BinaryFileSystem diff --git a/Code/systems/bin_system/bin_system.py b/Code/systems/bin_system/bin_system.py deleted file mode 100644 index 6e51d51..0000000 --- a/Code/systems/bin_system/bin_system.py +++ /dev/null @@ -1,76 +0,0 @@ -import atexit -import os -import pickle -from typing import Any, Dict, Optional - -from root_path import ROOT_PATH - - -class BinaryFileSystem: - __slots__ = ['_data', '_file_path'] - - def __init__(self, file_path: Optional[str], file_name: str) -> None: - """Инициализирует объект BinaryFileSystem. - На текущий момент данный класс используется как сохранение настроек - - Args: - file_path (Optional[str]): Путь до файла с данными - file_name (str): Имя файла - """ - if not file_path: - file_path = "" - - full_file_path = os.path.join(ROOT_PATH, "data", full_file_path) - - if not os.path.exists(full_file_path): - os.makedirs(full_file_path) - - self._file_path = os.path.join(full_file_path, f"{file_name}.bin") - - self._data = {} - try: - self._read_file() - - except Exception: - self._write_file() - - atexit.register(self._write_file) - - def _write_file(self) -> None: - """Записывает данные в файл. - """ - with open(self._file_path, 'wb') as file: - pickle.dump(self._data, file) - - def _read_file(self) -> Dict[str, Any]: - """Читает данные из файла - - Returns: - Dict[str, Any]: Словарь данных - """ - with open(self._file_path, 'rb') as file: - self._data = pickle.load(file) - - def __getitem__(self, key) -> Any: - """Возвращает значение по ключу. - - Args: - key (str): Ключ для доступа к значению. - - Returns: - Any: Значение, соответствующее ключу. - """ - return self._data.get(key, None) - - def __setitem__(self, key: str, value: Any) -> None: - """Устанавливает значение по ключу и сохраняет изменения в файл, если значение отличается от текущего. - - Args: - key (str): Ключ для установки значения. - value (Any): Устанавливаемое значение. - """ - if key in self._data and self._data.get(key, None) == value: - return - - self._data[key] = value - self._write_file() diff --git a/Code/systems/events_system/__init__.py b/Code/systems/events_system/__init__.py deleted file mode 100644 index fda0e91..0000000 --- a/Code/systems/events_system/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from systems.events_system.event_manager import EventManager -from systems.events_system.register import register_events - -__all__ = ['EventManager', 'register_events'] diff --git a/Code/systems/events_system/event_manager.py b/Code/systems/events_system/event_manager.py deleted file mode 100644 index dff113d..0000000 --- a/Code/systems/events_system/event_manager.py +++ /dev/null @@ -1,46 +0,0 @@ -import asyncio -from inspect import signature -from typing import Any, Callable, Dict, List - -from systems.misc import GlobalClass - - -class EventManager(GlobalClass): - __slots__ = ['_open_events', '_protected_events', '_initialized'] - - def __init__(self) -> None: - if not hasattr(self, '_initialized'): - self._initialized = True - self._open_events: Dict[str, List[Callable[..., Any]]] = {} - self._protected_events: Dict[str, List[Callable[..., Any]]] = {} - - def _register_event(self, event_dict: Dict[str, List[Callable[..., Any]]], event_name: str, func: Callable[..., Any]) -> None: - if event_name not in event_dict: - event_dict[event_name] = [] - event_dict[event_name].append(func) - - def register_open_event(self, event_name: str, func: Callable[..., Any]) -> None: - self._register_event(self._open_events, event_name, func) - - def register_protected_event(self, event_name: str, func: Callable[..., Any]) -> None: - self._register_event(self._protected_events, event_name, func) - - async def _call_event(self, event_dict: Dict[str, List[Callable[..., Any]]], event_name: str, *args, **kwargs) -> None: - handlers = event_dict.get(event_name, []) - for handler in handlers: - handler_signature = signature(handler) - handler_kwargs = {k: v for k, v in kwargs.items() if k in handler_signature.parameters} - if asyncio.iscoroutinefunction(handler): - await handler(*args, **handler_kwargs) - else: - handler(*args, **handler_kwargs) - - async def call_open_event(self, event_name: str, *args, **kwargs) -> None: - await self._call_event(self._open_events, event_name, *args, **kwargs) - - async def call_protected_event(self, event_name: str, *args, **kwargs) -> None: - await self._call_event(self._protected_events, event_name, *args, **kwargs) - - async def call_all_event(self, event_name: str, *args, **kwargs) -> None: - await self._call_event(self._open_events, event_name, *args, **kwargs) - await self._call_event(self._protected_events, event_name, *args, **kwargs) diff --git a/Code/systems/events_system/events/__init__.py b/Code/systems/events_system/events/__init__.py deleted file mode 100644 index a8aa4e0..0000000 --- a/Code/systems/events_system/events/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from systems.events_system.events.test import test_echo - -__all__ = ['test_echo'] diff --git a/Code/systems/events_system/events/test.py b/Code/systems/events_system/events/test.py deleted file mode 100644 index 9c839ef..0000000 --- a/Code/systems/events_system/events/test.py +++ /dev/null @@ -1,10 +0,0 @@ -import logging - -logger = logging.getLogger("Test ev") - -#TODO: Удалить после добавления первого нормального ивента - -def test_echo(data): # Ивенты работают только по именованным аргументам! Если сервер отправил data, то и в функцию прийдёт только data. socket_user, socket_access: UserAccess всегда можно поставить в функцию - logger.info(f"Echo event called with data: {data}") - -test_echo.event_name = "echo" # Указание типа ивента diff --git a/Code/systems/events_system/register.py b/Code/systems/events_system/register.py deleted file mode 100644 index 1d8aec7..0000000 --- a/Code/systems/events_system/register.py +++ /dev/null @@ -1,46 +0,0 @@ -import importlib.util -import logging -import re -from pathlib import Path - -from root_path import ROOT_PATH -from systems.events_system.event_manager import EventManager - -logger = logging.getLogger("Event manager") - -def import_module_from_file(module_name, file_path): - spec = importlib.util.spec_from_file_location(module_name, file_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - -def register_events() -> None: - logger.info("Start register events") - - ev_manager = EventManager() - event_name_pattern = re.compile(r"^(?:o_|p_)?(.+?)_event$") - - events_directory = Path(ROOT_PATH) - events_directory = events_directory / "Code" / "systems" / "events_system" / "events" - for file_path in events_directory.rglob("*.py"): - module_name = file_path.stem # Имя модуля без расширения .py - if module_name == "__init__": - continue # Иначе будет регестрация по 15 раз одного и того же - - module = import_module_from_file(module_name, file_path) - - for name in dir(module): - handler = getattr(module, name) - if callable(handler): - match = event_name_pattern.match(name) - if match: - event_name = match.group(1) # Извлекаем имя события без модификаторов и суффиксов - if name.startswith("o_"): - ev_manager.register_open_event(event_name, handler) - logger.info(f"Registered open event '{event_name}' with handler '{handler.__name__}'") - - elif name.startswith("p_"): - ev_manager.register_protected_event(event_name, handler) - logger.info(f"Registered protected event '{event_name}' with handler '{handler.__name__}'") - - logger.info("End register events") diff --git a/Code/systems/loc/__init__.py b/Code/systems/loc/__init__.py new file mode 100644 index 0000000..95da41e --- /dev/null +++ b/Code/systems/loc/__init__.py @@ -0,0 +1 @@ +from .loc import Localization diff --git a/Code/systems/loc/loc.py b/Code/systems/loc/loc.py new file mode 100644 index 0000000..abccedd --- /dev/null +++ b/Code/systems/loc/loc.py @@ -0,0 +1,133 @@ +from pathlib import Path +from typing import Dict, Optional + +""" +Пример .loc +main-app-name={form-apple} {sex-apple} # комментарий + .form1-apple=яблоко + .form2-apple=яблока + .form5-apple=яблок + .male-apple=Он + .female-apple=Она + .neuter-apple=Оно +""" + +class Localization: + _translations: Dict[str, str] = {} + + @classmethod + def clear_load_translation(cls) -> None: + """Очищает все загруженные переводы.""" + cls._translations.clear() + + @classmethod + def load_translations(cls, folder_path: str) -> None: + """Рекурсивно загружает все файлы локализации (.loc) из указанной папки. + + Args: + folder_path (str): Путь к папке, содержащей файлы локализации. + """ + folder = Path(folder_path) + for file_path in folder.rglob("*.loc"): + cls._load_file(file_path) + + @classmethod + def _load_file(cls, file_path: Path) -> None: + """Загружает и парсит файл локализации, добавляя переводы в словарь. + + Args: + file_path (Path): Путь к файлу локализации (.loc). + """ + with file_path.open('r', encoding='utf-8') as file: + current_key: Optional[str] = None + for line in file: + line = line.strip() + + # Убираем комментарий, если # не экранирован + if "#" in line: + line = cls._remove_comment(line) + + if "=" in line and not line.startswith("."): + current_key, value = line.split('=', 1) + cls._translations[current_key.strip()] = value.strip() + + elif line.startswith(".") and current_key: + sub_key, sub_value = line.split('=', 1) + cls._translations[sub_key.strip()] = sub_value.strip() + + @staticmethod + def _remove_comment(line: str) -> str: + """Удаляет комментарий из строки, если # не экранирован. + + Args: + line (str): Строка с возможным комментарием. + + Returns: + str: Строка без комментария. + """ + if r"\#" in line: + # Если # экранирован, заменяем его на специальный маркер + line = line.replace(r"\#", "__TEMP_HASH__") + + # Оставляем только часть строки до первого # + line = line.split("#", 1)[0].strip() + + # Возвращаем экранированный # обратно + return line.replace("__TEMP_HASH__", "#") + + @staticmethod + def _select_form(count: int, base_key: str) -> str: + """Определяет форму слова на основе числа. + + Args: + count (int): Количество, определяющее форму слова. + base_key (str): Базовый ключ слова, для которого выбирается форма. + + Returns: + str: Ключ формы слова ('form1', 'form2', 'form5'). + """ + if count % 10 == 1 and count % 100 != 11: + return f'form1-{base_key}' + + elif 2 <= count % 10 <= 4 and not 12 <= count % 100 <= 14: + return f'form2-{base_key}' + + else: + return f'form5-{base_key}' + + @classmethod + def get_string(cls, key: str, **kwargs: Dict[str, Dict[str, Optional[int]]]) -> str: + """Возвращает строку перевода с подстановкой форм слов. + + Args: + key (str): Ключ основной строки локализации. + **kwargs: Дополнительные параметры для подстановки форм и рода. + + Returns: + str: Строка с подставленными формами и родом или сообщение об отсутствии ключа. + + Examples:: + + result = Localization.get_string( + 'main-app-name', + key1={'count': 3, 'gender': 'female'}, + key2={'count': 1, 'gender': 'male'} + ) + """ + text: str = cls._translations.get(key, f"[Missing key: {key}]") + + for k, v in kwargs.items(): + if isinstance(v, dict): + count: Optional[int] = v.get('count') + if count is not None: + form_key: str = Localization._select_form(count, k) + form_value: str = cls._translations.get(form_key, f"[Missing form: {form_key}]") + text = text.replace(f"{{form-{k}}}", form_value) + + gender: Optional[str] = v.get('gender') + if gender is not None: + gender_key: str = f"{gender}-{k}" + gender_value: str = cls._translations.get(gender_key, f"[Missing gender: {gender_key}]") + text = text.replace(f"{{sex-{k}}}", gender_value) + + return text diff --git a/Code/systems/misc.py b/Code/systems/misc.py deleted file mode 100644 index 8fd4443..0000000 --- a/Code/systems/misc.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any - - -class GlobalClass: - _instance: Any = None - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - cls._instance = super(GlobalClass, cls).__new__(cls) - - return cls._instance - - # Данную дичь прописывать в каждом глобальном классе. И мне похуй - #def __init__(self, *args, **kwargs): - # if not hasattr(self, '_initialized'): - # self._initialized = True diff --git a/Code/systems/network/__init__.py b/Code/systems/network/__init__.py deleted file mode 100644 index 965c4fa..0000000 --- a/Code/systems/network/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from systems.network.client_unit import ClientUnit diff --git a/Code/systems/network/client_unit.py b/Code/systems/network/client_unit.py deleted file mode 100755 index fe0c9d0..0000000 --- a/Code/systems/network/client_unit.py +++ /dev/null @@ -1,183 +0,0 @@ -import asyncio -import atexit -import os -import shutil -import zipfile -from typing import Any, Dict, Optional - -import msgpack -import requests -import websockets -from root_path import ROOT_PATH -from systems.events_system import EventManager -from systems.misc import GlobalClass - - -class ClientUnit(GlobalClass): - __slots__ = ['_initialized', '_http_url', '_socket_url', '_session', '_token', '_cur_server_name', '_websocket', '_bg_processing', '_bg_task'] - SOCKET_CHUNK_SIZE: int = 8192 - DEFAULT_DOWNLOAD_CHUNK_SIZE: int = 8192 - - def __init__(self) -> None: - if not hasattr(self, '_initialized'): - self._initialized = True - self._http_url: str = "" - self._socket_url: str = "" - self._session: requests.Session = requests.Session() - - self._token: Optional[str] = None - self._cur_server_name: Optional[str] = None - - self._websocket: Optional[websockets.WebSocketClientProtocol] = None - self._bg_processing: bool = False - self._bg_task: Optional[asyncio.Task] = None - - atexit.register(self._shutdown) - - # --- Net data --- # - @staticmethod - def pack_data(data: Any) -> bytes: - return msgpack.packb(data) - - @staticmethod - def unpack_data(data: bytes) -> Any: - return msgpack.unpackb(data) - - # --- Server API --- # - def check_server(self, ip: str, port: int = 5000) -> None: - temp_http_url = f"http://{ip}:{port}" - response = self._session.get(f"{temp_http_url}/server/check_status", timeout=5) - response.raise_for_status() - - response_data: dict = response.json() - if response_data.get("message") == "Server is online": - server_info: dict = response_data.get("server_info", {}) - self._http_url = temp_http_url - self._socket_url = f"ws://{ip}:{server_info.get('socket_port')}" - self._cur_server_name = server_info.get("server_name") - - def download_server_content(self, progress_callback=None) -> None: - try: - response = self._session.get(f"{self._http_url}/server/download_server_content", stream=True) - response.raise_for_status() - - except requests.RequestException as e: - raise RuntimeError(f"Error during HTTP request: {e}") - - archive_path = "content.zip" - content_dir = os.path.join(ROOT_PATH, 'Content', "Servers", self._cur_server_name) - total_size = int(response.headers.get('content-length', 0)) - downloaded_size = 0 - - try: - with open(archive_path, 'wb') as file: - for chunk in response.iter_content(chunk_size=self.DEFAULT_DOWNLOAD_CHUNK_SIZE): - file.write(chunk) - downloaded_size += len(chunk) - if progress_callback: - progress_callback(downloaded_size, total_size) - - except IOError as e: - raise IOError(f"Error saving file: {e}") - - if os.path.exists(content_dir): - shutil.rmtree(content_dir) - - os.makedirs(content_dir, exist_ok=True) - - try: - with zipfile.ZipFile(archive_path, 'r') as zip_ref: - zip_ref.extractall(content_dir) - - except zipfile.BadZipFile as e: - raise RuntimeError(f"Error extracting zip file: {e}") - - finally: - os.remove(archive_path) - - # --- Auth API --- # - def register(self, login: str, password: str) -> None: - self._auth_request("register", login, password) - self.login(login, password) - - def login(self, login: str, password: str) -> None: - token = self._auth_request("login", login, password)["token"] - self._session.headers.update({"Authorization": token}) - self._token = token - - def logout(self) -> None: - if self._token: - response = self._session.post(f"{self._http_url}/auth/logout") - self._session.headers.update({"Authorization": None}) - response.raise_for_status() - - def _auth_request(self, endpoint: str, login: str, password: str) -> Dict[str, Any]: - response = self._session.post(f"{self._http_url}/auth/{endpoint}", json={'login': login, 'password': password}) - response.raise_for_status() - return response.json() - - # --- WebSocket work --- # - async def connect(self) -> None: - if not self._token: - raise ConnectionError("Token is not available. Please log in first.") - - self._websocket = await websockets.connect(self._socket_url) - await self._websocket.send(self._token) - - # Wait for token confirmation - response = await self._websocket.recv() - if response != "Token accepted": - raise ConnectionError("Token was not accepted by the server") - - async def disconnect(self) -> None: - await self.stop_bg_processing() - if self._websocket: - await self._websocket.close() - self._websocket = None - - async def send_data(self, data: Dict[str, Any]) -> None: - if not self._websocket: - raise ConnectionError("WebSocket connection is not established") - packed_data = self.pack_data(data) - await self._websocket.send(packed_data) - - async def recv_data(self) -> Dict[str, Any]: - if not self._websocket: - raise ConnectionError("WebSocket connection is not established") - response = await self._websocket.recv() - return self.unpack_data(response) - - # --- Background Processing --- # - async def start_bg_processing(self) -> None: - if not self._websocket: - raise ConnectionError("WebSocket connection is not established") - if self._bg_processing: - raise ValueError("Background processing is already running") - - self._bg_processing = True - self._bg_task = asyncio.create_task(self._bg_receive()) - - async def stop_bg_processing(self) -> None: - if self._bg_processing: - self._bg_processing = False - if self._bg_task: - self._bg_task.cancel() - await self._bg_task - self._bg_task = None - - async def _bg_receive(self) -> None: - ev_manager = EventManager() - while self._bg_processing: - try: - data: dict = await self.recv_data() - await ev_manager.call_open_event(event_name=data.get('ev_type', None), **data) - except asyncio.CancelledError: - break - except Exception as e: - # Log or handle the exception - pass - - # --- Cleanup --- # - def _shutdown(self) -> None: - self.logout() - asyncio.run(self.disconnect()) diff --git a/Code/systems/texture_system/__init__.py b/Code/systems/texture_system/__init__.py index ff1c5e5..a432a68 100644 --- a/Code/systems/texture_system/__init__.py +++ b/Code/systems/texture_system/__init__.py @@ -1,4 +1,3 @@ -from systems.texture_system.color import Color -from systems.texture_system.texture_system import TextureSystem +from .texture_system import TextureSystem -__all__ = ['Color', 'TextureSystem'] +__all__ = ['TextureSystem'] diff --git a/Code/systems/texture_system/color.py b/Code/systems/texture_system/color.py deleted file mode 100644 index c362f6c..0000000 --- a/Code/systems/texture_system/color.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Tuple - - -class Color: - __slots__ = ['_r', '_g', '_b', '_a'] - - def __init__(self, r: int, g: int, b: int, a: int) -> None: - self.r = r - self.g = g - self.b = b - self.a = a - - @property - def r(self) -> int: - return self._r - - @r.setter - def r(self, value: int) -> None: - if not (0 <= value <= 255): - raise ValueError("Invalid value for r. It must be between 0 and 255") - - self._r = value - - @property - def g(self) -> int: - return self._g - - @g.setter - def g(self, value: int) -> None: - if not (0 <= value <= 255): - raise ValueError("Invalid value for g. It must be between 0 and 255") - - self._g = value - - @property - def b(self) -> int: - return self._b - - @b.setter - def b(self, value: int) -> None: - if not (0 <= value <= 255): - raise ValueError("Invalid value for b. It must be between 0 and 255") - - self._b = value - - @property - def a(self) -> int: - return self._a - - @a.setter - def a(self, value: int) -> None: - if not (0 <= value <= 255): - raise ValueError("Invalid value for a. It must be between 0 and 255") - - self._a = value - - def __str__(self) -> str: - return f"{self.r}_{self.g}_{self.b}_{self.a}" - - def __repr__(self) -> str: - return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})" - - @staticmethod - def from_tuple(value: Tuple[int, int, int, int]) -> "Color": - return Color(value[0], value[1], value[2], value[3]) - - def to_tuple(self) -> Tuple[int, int, int, int]: - return (self.r, self.g, self.r, self.a) diff --git a/Code/systems/texture_system/texture_system.py b/Code/systems/texture_system/texture_system.py index 65841a9..d851e6c 100644 --- a/Code/systems/texture_system/texture_system.py +++ b/Code/systems/texture_system/texture_system.py @@ -4,9 +4,8 @@ from typing import Any, Dict, List, Optional, Tuple, Union import yaml +from DMBotTools import Color from PIL import Image, ImageSequence -from root_path import ROOT_PATH -from systems.texture_system.color import Color class TextureSystem: @@ -255,7 +254,7 @@ def merge_images(background: Image.Image, overlay: Image.Image, position: Tuple[ return merged_image @staticmethod - def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[Image.Image, List[Image.Image]]: + def merge_layers(root_path, layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[Image.Image, List[Image.Image]]: """Объединяет слои в одно изображение или GIF. Args: @@ -265,7 +264,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[ Returns: Union[Image.Image, List[Image.Image]]: Объединенное изображение или список кадров GIF. """ - base_path = os.path.join(ROOT_PATH, 'Content', 'Compiled') + base_path = os.path.join(root_path, 'Content', 'Compiled') if not os.path.exists(base_path): os.makedirs(base_path) @@ -311,7 +310,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[ final_images[i] = final_image_expanded else: if is_mask: - final_image = TextureSystem.get_image_recolor(first_layer['path'], first_layer['state'], Color.from_tuple(first_layer['color'])) + final_image = TextureSystem.get_image_recolor(first_layer['path'], first_layer['state'], Color(*first_layer['color'])) else: final_image = TextureSystem.get_image(first_layer['path'], first_layer['state']) @@ -325,7 +324,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[ if is_gif: if is_mask: - recolored_frames = TextureSystem.get_gif_recolor(layer['path'], layer['state'], Color.from_tuple(layer['color']), fps) + recolored_frames = TextureSystem.get_gif_recolor(layer['path'], layer['state'], Color(*layer['color']), fps) for i in range(max_frames): recolored_frame_expanded = Image.new("RGBA", (max_width, max_height)) frame_to_use = recolored_frames[min(i, len(recolored_frames) - 1)] # Используем последний кадр, если i превышает количество кадров @@ -346,7 +345,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[ final_images.append(normal_frame_expanded) else: if is_mask: - recolored_image = TextureSystem.get_image_recolor(layer['path'], layer['state'], Color.from_tuple(layer['color'])) + recolored_image = TextureSystem.get_image_recolor(layer['path'], layer['state'], Color(*layer['color'])) recolored_image_expanded = Image.new("RGBA", (max_width, max_height)) recolored_image_expanded.paste(recolored_image, (0, 0)) for i in range(len(final_images)): diff --git a/Content/Client/fronts/Monocraft/COPYRIGHT.txt b/Content/Client/fronts/Monocraft/COPYRIGHT.txt new file mode 100644 index 0000000..a67717a --- /dev/null +++ b/Content/Client/fronts/Monocraft/COPYRIGHT.txt @@ -0,0 +1,2 @@ +Idrees Hassan +https://github.com/IdreesInc/Monocraft \ No newline at end of file diff --git a/Content/Client/fronts/Monocraft/LICENSE.txt b/Content/Client/fronts/Monocraft/LICENSE.txt new file mode 100644 index 0000000..075d1ae --- /dev/null +++ b/Content/Client/fronts/Monocraft/LICENSE.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/Content/Client/fronts/Monocraft/Monocraft.otf b/Content/Client/fronts/Monocraft/Monocraft.otf new file mode 100644 index 0000000000000000000000000000000000000000..631c97a56e156ebf138ceacaa9a379ce8e1e3b5a GIT binary patch literal 22020 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 - - AuthDialog - - - - 0 - 0 - 400 - 300 - - - - - 400 - 300 - - - - - 400 - 300 - - - - Авторизация - - - - - 20 - 20 - 360 - 260 - - - - - - - - 64 - 64 - - - - Введите свой логин - - - - - - - - 64 - 64 - - - - Введите свой пароль - - - - - - - - 64 - 64 - - - - Войти - - - - - - - - - diff --git a/GUI/windows/LoginWindow.ui b/GUI/windows/LoginWindow.ui deleted file mode 100644 index 6a531e9..0000000 --- a/GUI/windows/LoginWindow.ui +++ /dev/null @@ -1,167 +0,0 @@ - - - LoginWindow - - - true - - - - 0 - 0 - 640 - 640 - - - - - 640 - 640 - - - - - 640 - 640 - - - - - 0 - 0 - - - - DM-Bot / Login... - - - - - true - - - - 410 - 20 - 64 - 64 - - - - - 64 - 64 - - - - - 64 - 64 - - - - Qt::LeftToRight - - - Иконка старута - - - -4 - - - - - - 290 - 20 - 110 - 70 - - - - Проверить - - - - - - 20 - 20 - 260 - 70 - - - - - - - false - - - Введите IP сервера (127.0.0.1) - - - - - - - Введите порт сервера (5000) - - - - - - - - - 20 - 110 - 590 - 80 - - - - - - - - 0 - 64 - - - - Войти в аккаунт - - - - - - - - 0 - 64 - - - - Создать аккаунт - - - - - - - - - - 0 - 0 - 640 - 23 - - - - - - - - diff --git a/GUI/windows/MainWindow.ui b/GUI/windows/MainWindow.ui deleted file mode 100644 index c6854ca..0000000 --- a/GUI/windows/MainWindow.ui +++ /dev/null @@ -1,31 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 800 - 600 - - - - MainWindow - - - - - - 0 - 0 - 800 - 23 - - - - - - - - diff --git a/GUI/windows/RegistrationDialog.ui b/GUI/windows/RegistrationDialog.ui deleted file mode 100644 index ee3ee2c..0000000 --- a/GUI/windows/RegistrationDialog.ui +++ /dev/null @@ -1,86 +0,0 @@ - - - RegistrationDialog - - - - 0 - 0 - 400 - 300 - - - - Регистрация - - - - - 20 - 20 - 360 - 260 - - - - - - - - 0 - 56 - - - - Введите свой логин - - - - - - - - 0 - 56 - - - - - - - Введите пароль - - - - - - - - 0 - 56 - - - - Введите пароль повторно - - - - - - - - 0 - 56 - - - - Зарегестрироваться - - - - - - - - - diff --git a/GUI/windows/ServerContentDownloadDialog.ui b/GUI/windows/ServerContentDownloadDialog.ui deleted file mode 100644 index 31ff26a..0000000 --- a/GUI/windows/ServerContentDownloadDialog.ui +++ /dev/null @@ -1,60 +0,0 @@ - - - ServerContentDownloadDialog - - - - 0 - 0 - 400 - 150 - - - - - 400 - 150 - - - - - 400 - 150 - - - - Загрузка контента сервера - - - - - 20 - 20 - 360 - 80 - - - - - - - false - - - Загрузка контента сервера... - - - - - - - 24 - - - - - - - - - diff --git a/Tests/Texture/TextureSystem.py b/Tests/Texture/TextureSystem.py index 1be76f6..277a5c1 100644 --- a/Tests/Texture/TextureSystem.py +++ b/Tests/Texture/TextureSystem.py @@ -5,9 +5,11 @@ import unittest import yaml +from DMBotTools import Color from PIL import Image -from Code.systems.texture_system import * +from Code.root_path import ROOT_PATH +from Code.systems.texture_system import TextureSystem class TestTextureSystem(unittest.TestCase): @@ -189,7 +191,7 @@ def test_merge_layers(self): {'path': self.test_dir, 'state': 'state1', 'color': (255, 0, 0, 255)}, {'path': self.test_dir, 'state': 'state3', 'color': (0, 255, 0, 255)} ] - result_image = TextureSystem.merge_layers(layers) + result_image = TextureSystem.merge_layers(ROOT_PATH, layers) self.assertIsNotNone(result_image) self.assertEqual(result_image.size, (200, 200)) @@ -197,7 +199,7 @@ def test_merge_layers(self): {'path': self.test_dir, 'state': 'state2', 'color': (255, 0, 0, 255)}, {'path': self.test_dir, 'state': 'state4', 'color': (0, 255, 0, 255)} ] - result_gif = TextureSystem.merge_layers(layers) + result_gif = TextureSystem.merge_layers(ROOT_PATH, layers) self.assertIsNotNone(result_gif) self.assertTrue(isinstance(result_gif, list)) self.assertGreater(len(result_gif), 0) diff --git a/requirements.txt b/requirements.txt index e8dd3fb..aa5226c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ +dearpygui +dearpygui-async +DMBotNetwork +DMBotTools msgpack Pillow -pydub -PyQt6 PyYAML -requests -websockets