diff --git a/.github/workflows/nuitka.yml b/.github/workflows/nuitka.yml index 94b2ac6..433dcd1 100644 --- a/.github/workflows/nuitka.yml +++ b/.github/workflows/nuitka.yml @@ -5,9 +5,7 @@ name: Build on: # 触发条件 push: - branches: ["main"] pull_request: - branches: ["main"] workflow_dispatch: # 主任务 @@ -80,18 +78,6 @@ jobs: Write-Output $infos >> $env:GITHUB_ENV python .github/workflows/get_info.py - # 这堆构建脚本全都是我自己写的 - - name: DR-rs build - shell: pwsh - run: | - cd libs/Difficult_Rocket_rs/src - python setup.py build - python post_build.py - python setup.py clean - cd .. - cd .. - cd .. - # 还是得我自己写脚本 - name: Build on Windows if: runner.os == 'Windows' @@ -165,7 +151,6 @@ jobs: Copy-Item libs/fonts build\DR.dist\libs\fonts -Recurse # Copy-Item libs\pyglet\ build\DR.dist -Recurse - # Copy-Item -Path "libs\Difficult_Rocket_rs\lib\*" -Destination "build\DR.dist\libs\Difficult_Rocket_rs\lib" -Recurse Rename-Item build/DR.dist Difficult-Rocket python ./.github/workflows/post_compile.py diff --git a/.vscode/settings.json b/.vscode/settings.json index d2b2620..670301a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,9 @@ "rust-analyzer.linkedProjects": [ "libs/Difficult_Rocket_rs/src/Cargo.toml", "libs/pyglet_rs/src/Cargo.toml", - ] + ], + "python.analysis.extraPaths": [ + "./libs" + ], + "python.analysis.typeCheckingMode": "basic" } diff --git a/DR.py b/DR.py index fcc7634..eabe5d0 100644 --- a/DR.py +++ b/DR.py @@ -7,6 +7,7 @@ import sys import time import cProfile import traceback +import threading from io import StringIO @@ -39,7 +40,7 @@ def print_path() -> None: # 输出一遍大部分文件位置相关信息 以后可能会加到logs里 -def main() -> None: +def main() -> int: print(hi) # hi! start_time_ns = time.time_ns() start_time_perf_ns = time.perf_counter_ns() @@ -57,7 +58,7 @@ def main() -> None: print('pyglet_rs available:', get_version_str()) print('trying to patch pyglet_rs') patch_vector() - except ImportError as e: + except ImportError: print('pyglet_rs import error') traceback.print_exc() try: @@ -82,7 +83,8 @@ def main() -> None: if DR_option.crash_report_test: raise TestError('debugging') # debug 嘛,试试crash except Exception as exp: # 出毛病了 - print(error_format['error.happen']) # + # 解析错误信息 + print(error_format['error.happen']) error = traceback.format_exc() name = type(exp).__name__ if name in error_format: @@ -90,6 +92,7 @@ def main() -> None: else: print(error_format['error.unknown']) print(error) + # 输出 crash 信息 crash.create_crash_report(error) cache_steam = StringIO() crash.write_info_to_cache(cache_steam) @@ -99,8 +102,20 @@ def main() -> None: crash.record_thread = False print(crash.all_thread) print(crash.all_process) + # join all thread + for thread in threading.enumerate(): + print(thread) + if thread.name == 'MainThread' or thread == threading.main_thread() or thread == threading.current_thread(): + continue + if thread.daemon: + continue + thread.join() + # stop pyglet + import pyglet + pyglet.app.exit() print("Difficult_Rocket 已关闭") + return 0 if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/Difficult_Rocket/__init__.py b/Difficult_Rocket/__init__.py index 0a51093..5aadb0a 100644 --- a/Difficult_Rocket/__init__.py +++ b/Difficult_Rocket/__init__.py @@ -4,7 +4,6 @@ # All rights reserved # ------------------------------- -import os import sys import warnings import importlib @@ -15,22 +14,19 @@ from pathlib import Path from typing import Optional, List, Tuple from Difficult_Rocket.api.types import Options +from Difficult_Rocket.utils.new_thread import new_thread from libs.MCDR.version import Version game_version = Version("0.7.2.2") # 游戏版本 build_version = Version("1.2.1.0") # 编译文件版本(与游戏本体无关) -Api_version = Version("0.0.2.0") # API 版本 +Api_version = Version("0.1.0.0") # API 版本 __version__ = game_version -# TODO 解耦 DR SDK 与 DR_mod 和 DR_rs -DR_rust_version = Version("0.2.6.1") # DR 的 Rust 编写部分的版本 -# 后面会移除的 DR_rs 相关信息 -# DR_rs和 DR_mod 的部分正在和 DR SDK 解耦 - -long_version: int = 14 +long_version: int = 15 """ long_version: 一个用于标记内部协议的整数 +15: 完全移除 DR_rust 相关内容 解耦完成 14: BaseScreen 的每一个函数都添加了一个参数: window: "ClientWindow" 13: 为 DR_runtime 添加 API_version 12: 去除 DR_runtime 的 global_logger @@ -66,36 +62,15 @@ class _DR_option(Options): DR_rust_available: bool = False use_cProfile: bool = False use_local_logging: bool = False - use_DR_rust: bool = True # tests playing: bool = False debugging: bool = False - crash_report_test: bool = True + crash_report_test: bool = False # window option gui_scale: float = 1.0 # default 1.0 2.0 -> 2x 3 -> 3x - def init(self, **kwargs): - try: - from libs.Difficult_Rocket_rs import test_call, get_version_str - test_call(self) - print(f'DR_rust available: {get_version_str()}') - except ImportError: - if __name__ != '__main__': - traceback.print_exc() - self.DR_rust_available = False - self.use_DR_rust = self.use_DR_rust and self.DR_rust_available - self.flush_option() - - def test_rust(self): - if self.DR_rust_available: - from libs.Difficult_Rocket_rs import part_list_read_test - part_list_read_test("./configs/PartList.xml") - - def draw(self): - self.DR_rust_available = True - @property def std_font_size(self) -> int: return round(12 * self.gui_scale) @@ -114,9 +89,6 @@ class _DR_runtime(Options): DR_long_version: int = long_version # DR SDK 内部协议版本 (不要问我为什么不用 Version,我也在考虑) DR_Mod_List: List[Tuple[str, Version]] = [] # DR Mod 列表 (name, version) - - DR_Rust_version: Version = DR_rust_version # 后面要去掉的 DR_rs 版本 - DR_Rust_get_version: Optional[Version] = None # 后面也要去掉的 DR_rs 版本 # run status running: bool = False @@ -132,15 +104,6 @@ class _DR_runtime(Options): language: str = 'zh-CN' default_language: str = 'zh-CN' - def init(self, **kwargs) -> None: - with contextlib.suppress(ImportError): - from libs.Difficult_Rocket_rs import get_version_str - self.DR_Rust_get_version = Version(get_version_str()) - if self.DR_Rust_get_version != self.DR_Rust_version: - relationship = 'larger' if self.DR_Rust_version > self.DR_Rust_get_version else 'smaller' - warnings.warn(f'DR_rust builtin version is {self.DR_Rust_version} but true version is {get_version_str()}.\n' - f'Builtin version {relationship} than true version') - def load_file(self) -> bool: with contextlib.suppress(FileNotFoundError): with open('./configs/main.toml', 'r', encoding='utf-8') as f: @@ -151,33 +114,34 @@ class _DR_runtime(Options): return True return False - def load_mods(self) -> None: - mod_list = self.find_mods() - def find_mods(self) -> List[str]: mods = [] - paths = Path(self.mod_path).iterdir() + mod_path = Path(self.mod_path) + if not mod_path.exists(): + mod_path.mkdir() + return [] + paths = mod_path.iterdir() sys.path.append(self.mod_path) for mod_path in paths: try: if mod_path.is_dir() and mod_path.name != '__pycache__': # 处理文件夹 mod if importlib.util.find_spec(mod_path.name) is not None: - module = importlib.import_module(mod_path.name) mods.append(mod_path.name) else: print(f'can not import mod {mod_path} because importlib can not find spec') elif mod_path.suffix in ('.pyz', '.zip'): # 处理压缩包 mod if importlib.util.find_spec(mod_path.name) is not None: - module = importlib.import_module(mod_path.name) + mods.append(mod_path.name) + elif mod_path.suffix == '.pyd': # pyd 扩展 mod + if importlib.util.find_spec(mod_path.name) is not None: mods.append(mod_path.name) elif mod_path.suffix == '.py': # 处理单文件 mod print(f'importing mod {mod_path=} {mod_path.stem}') - module = importlib.import_module(mod_path.stem) - mods.append(mod_path.stem) + if importlib.util.find_spec(mod_path.stem) is not None: + mods.append(mod_path.stem) except ImportError: print(f'ImportError when loading mod {mod_path}') traceback.print_exc() - self.DR_Mod_List = [(mod, Version('0.0.0-unknown')) for mod in mods] return mods @@ -185,9 +149,6 @@ DR_option = _DR_option() DR_runtime = _DR_runtime() if DR_option.playing: - from Difficult_Rocket.utils import new_thread - - def think_it(something): return something diff --git a/Difficult_Rocket/api/__init__.py b/Difficult_Rocket/api/__init__.py index 7b82bcc..ae9f186 100644 --- a/Difficult_Rocket/api/__init__.py +++ b/Difficult_Rocket/api/__init__.py @@ -11,7 +11,7 @@ github: @shenjackyuanjie gitee: @shenjackyuanjie """ -# 单独导入的(或者就这一个有用的) -# from .delivery import Delivery -# lazy之后之前全部导入的(太多了写不动__all__了 +# from Difficult_Rocket.api import screen, mod, exception + +__all__ = ['screen', 'mod', 'exception'] diff --git a/Difficult_Rocket/api/delivery.py b/Difficult_Rocket/api/delivery.py deleted file mode 100644 index d8f9b48..0000000 --- a/Difficult_Rocket/api/delivery.py +++ /dev/null @@ -1,28 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- - -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" - - -class Delivery: - def __init__(self): - # bool - self.using = False - self.read = False - self.key_board_get = False - # dic - self.ship_info = {} - self.planet_system = {} - self.main_ship_parts = {} - self.this_planet_info = {} - self.back_ground_element = {} - # value - self.back_ground_image = '' diff --git a/Difficult_Rocket/api/exception/__init__.py b/Difficult_Rocket/api/exception/__init__.py index 8fcca51..413b75d 100644 --- a/Difficult_Rocket/api/exception/__init__.py +++ b/Difficult_Rocket/api/exception/__init__.py @@ -10,3 +10,10 @@ mail: 3695888@qq.com github: @shenjackyuanjie gitee: @shenjackyuanjie """ + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from Difficult_Rocket.api.exception import command, logger, main, threading, unsupport + +__all__ = ['command', 'logger', 'main', 'threading', 'unsupport'] diff --git a/Difficult_Rocket/api/mod/__init__.py b/Difficult_Rocket/api/mod/__init__.py new file mode 100644 index 0000000..dcaf146 --- /dev/null +++ b/Difficult_Rocket/api/mod/__init__.py @@ -0,0 +1,81 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +# system function +from typing import Tuple, List, Optional, TypeVar, TYPE_CHECKING + +# from libs +from libs.MCDR.version import Version + +# from DR +if TYPE_CHECKING: + from Difficult_Rocket.main import Game + from Difficult_Rocket.client import ClientWindow +else: + Game = TypeVar("Game") + ClientWindow = TypeVar("ClientWindow") + +from Difficult_Rocket import DR_runtime +from ..types import Options + +RequireVersion = Tuple[Version, Version] +# 第一个是最低兼容版本,第二个是最高兼容版本 +# 例如: ("1.0.0", "1.1.0") 表示从1.0.0版本开始兼容,到1.1.0版本结束兼容 +ForceRequire = bool + + +class ModInfo(Options): + """ + 加载mod时候的参数 + """ + """基本信息""" + mod_id: str # mod id + name: str # mod 名称 + version: Version # mod 版本 + + """作者、描述""" + writer: str # 作者 + link: str = "" # 作者链接 + description: str = "" # 描述 (务必简洁明了) + info: str = "" # 其他信息 (可以很多很多) + + """版本相关信息""" + DR_version: RequireVersion = (DR_runtime.DR_version, DR_runtime.DR_version) # DR SDK 兼容版本 + DR_Api_version: RequireVersion = (DR_runtime.API_version, DR_runtime.API_version) # DR Api版本 + Mod_Require_version: List[Tuple[str, ForceRequire, RequireVersion]] = [] # mod 依赖版本 + + """mod 状态""" + is_enable: bool = True # 是否启用 + is_loaded: bool = False # 是否加载 + + """mod 配置""" + config: Options = Options() # mod 配置存储 + old_mod: Optional["ModInfo"] = None # 旧的mod实例 + + def on_load(self, game: Game, old_self: Optional["ModInfo"] = None) -> bool: + """ 加载时调用 """ + print(f'Mod {self.mod_id} loaded') + return True + + def on_client_start(self, game: Game, client: ClientWindow): + """ 客户端启动时调用 """ + print(f'Mod {self.mod_id} client start') + + def on_client_stop(self, game: Game, client: ClientWindow, source: str = 'window'): + """ 客户端停止时调用 """ + print(f'Mod {self.mod_id} client stop') + + def on_server_start(self, game: Game): + """ 服务器启动时调用 """ + print(f'Mod {self.mod_id} server start') + + def on_server_stop(self, game: Game): + """ 服务器停止时调用 """ + print(f'Mod {self.mod_id} server stop') + + def on_unload(self, game: Game): + """ 卸载时调用 """ + print(f'Mod {self.mod_id} unloaded') diff --git a/Difficult_Rocket/api/types/SR1/__init__.py b/Difficult_Rocket/api/types/SR1/__init__.py index fa05b12..89ec821 100644 --- a/Difficult_Rocket/api/types/SR1/__init__.py +++ b/Difficult_Rocket/api/types/SR1/__init__.py @@ -5,15 +5,14 @@ # ------------------------------- import math -from typing import Dict, Union, List, Optional +from typing import Dict, Union, Optional from dataclasses import dataclass # pyglet -# import pyglet from pyglet.image import load, AbstractImage # Difficult Rocket -from Difficult_Rocket.api.types import Options +from Difficult_Rocket.utils.options import Options @dataclass @@ -30,7 +29,6 @@ class SR1PartData: flip_y: bool explode: bool textures: Optional[str] = None - connections: Optional[List[int]] = None class SR1Textures(Options): @@ -145,23 +143,3 @@ def xml_bool(bool_like: Union[str, int, bool, None]) -> bool: if isinstance(bool_like, int): return bool_like != 0 return False if bool_like == '0' else bool_like.lower() != 'false' - -# -# -# from xml.etree.ElementTree import Element, ElementTree -# from defusedxml.ElementTree import parse -# -# part_list = parse("../../../../textures/PartList.xml") -# part_list_root: Element = part_list.getroot() -# print(part_list_root.tag, part_list_root.attrib) -# -# part_types = part_list_root.find('PartTypes') -# -# for x in list(part_list_root): -# print(f'tag: {x.tag} attr: {x.attrib}') -# -# for part_type in list(part_list_root): -# part_type: Element -# print(f'\'{part_type.attrib.get("id")}\': \'{part_type.attrib.get("sprite")}\'') -# -# diff --git a/Difficult_Rocket/api/types/__init__.py b/Difficult_Rocket/api/types/__init__.py index c36f55b..bf37d47 100644 --- a/Difficult_Rocket/api/types/__init__.py +++ b/Difficult_Rocket/api/types/__init__.py @@ -4,195 +4,24 @@ # All rights reserved # ------------------------------- -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" +from Difficult_Rocket.utils.options import Options, FontData, Fonts, \ + OptionsError, OptionNameNotDefined, OptionNotFound, \ + get_type_hints_ -import traceback -from dataclasses import dataclass -from typing import get_type_hints, Type, List, Union, Dict, Any, Callable, Tuple, Optional, TYPE_CHECKING +__all__ = [ + # main class + 'Options', -# from Difficult Rocket + # data class + 'FontData', + 'Fonts', -__all__ = ['get_type_hints_', - 'Options', - 'Fonts', - 'FontData', - 'OptionsError', - 'OptionNotFound', - 'OptionNameNotDefined'] + # exception + 'OptionsError', + 'OptionNameNotDefined', + 'OptionNotFound', + # other + 'get_type_hints_', +] -def get_type_hints_(cls: Type): - try: - return get_type_hints(cls) - except ValueError: - return get_type_hints(cls, globalns={}) - - -class OptionsError(Exception): - """ option 的错误基类""" - - -class OptionNameNotDefined(OptionsError): - """ 向初始化的 option 里添加了一个不存在于选项里的选项 """ - - -class OptionNotFound(OptionsError): - """ 某个选项没有找到 """ - - -class Options: - """ - Difficult Rocket 的游戏配置的存储基类 - """ - name = 'Option Base' - cached_options: Dict[str, Union[str, Any]] = {} - - def __init__(self, **kwargs): - """ - 创建一个新的 Options 的时候的配置 - 如果存在 init 方法 会在设置完 kwargs 之后运行子类的 init 方法 - :param kwargs: - """ - self.flush_option() - for option, value in kwargs.items(): - if option not in self.cached_options: - raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined") - setattr(self, option, value) - if hasattr(self, 'init'): - self.init(**kwargs) - if hasattr(self, 'load_file'): - try: - self.load_file() - except Exception: - traceback.print_exc() - self.flush_option() - - if TYPE_CHECKING: - def init(self, **kwargs) -> None: - """ 如果子类定义了这个函数,则会在 __init__ 之后调用这个函数 """ - - def load_file(self) -> bool: - """如果子类定义了这个函数,则会在 __init__ 和 init 之后再调用这个函数 - - 请注意,这个函数请尽量使用 try 包裹住可能出现错误的部分 - 否则会在控制台输出你的报错""" - - def option(self) -> Dict[str, Any]: - """ - 获取配置类的所有配置 - :return: 自己的所有配置 - """ - values = {} - for ann in self.__annotations__: # 获取类型注释 - values[ann] = getattr(self, ann, None) - if values[ann] is None: - values[ann] = self.__annotations__[ann] - - if not hasattr(self, 'options'): - self.options: Dict[str, Union[Callable, object]] = {} - for option, a_fun in self.options.items(): # 获取额外内容 - values[option] = a_fun - - for option, a_fun in values.items(): # 检查是否为 property - if a_fun is bool and getattr(self, option, None) is not None: - values[option] = False - if isinstance(a_fun, property): - try: - values[option] = getattr(self, option) - except AttributeError: - raise OptionNotFound(f'Option {option} is not found in {self.name}') from None - return values - - def format(self, text: str) -> str: - """ - 使用自己的选项给输入的字符串替换内容 - :param text: 想替换的内容 - :return: 替换之后的内容 - """ - cache_option = self.flush_option() - for option, value in cache_option.items(): - text = text.replace(f'{{{option}}}', str(value)) - return text - - def flush_option(self) -> Dict[str, Any]: - """ - 刷新缓存 options 的内容 - :return: 刷新过的 options - """ - self.cached_options = self.option() - return self.cached_options - - def option_with_len(self) -> List[Union[List[Tuple[str, Any, Any]], int, Any]]: - options = self.flush_option() - max_len_key = 1 - max_len_value = 1 - max_len_value_t = 1 - option_list = [] - for key, value in options.items(): - value_t = value if isinstance(value, Type) else type(value) - max_len_key = max(max_len_key, len(key)) - max_len_value = max(max_len_value, len(str(value))) - max_len_value_t = max(max_len_value_t, len(str(value_t))) - option_list.append((key, value, value_t)) - return [option_list, max_len_key, max_len_value, max_len_value_t] - - @classmethod - def add_option(cls, name: str, value: Union[Callable, object]) -> Dict: - if not hasattr(cls, 'options'): - cls.options: Dict[str, Union[Callable, object]] = {} - cls.options[name] = value - return cls.options - - @staticmethod - def init_option(options_class: 'Options'.__class__, init_value: Optional[dict] = None) -> 'Options': - return options_class(**init_value if init_value is not None else {}) - - -class Fonts(Options): - # font's value - - HOS: str = 'HarmonyOS Sans' - HOS_S: str = 'HarmonyOS Sans SC' - HOS_T: str = 'HarmonyOS Sans TC' - HOS_C: str = 'HarmonyOS Sans Condensed' - - 鸿蒙字体: str = HOS - 鸿蒙简体: str = HOS_S - 鸿蒙繁体: str = HOS_T - 鸿蒙窄体: str = HOS_C - - CC: str = 'Cascadia Code' - CM: str = 'Cascadia Mono' - CCPL: str = 'Cascadia Code PL' - CMPL: str = 'Cascadia Mono PL' - - 微软等宽: str = CC - 微软等宽无线: str = CM - 微软等宽带电线: str = CCPL - 微软等宽带电线无线: str = CMPL - - 得意黑: str = '得意黑' - # SS = smiley-sans - SS: str = 得意黑 - - -@dataclass -class FontData: - """ 用于保存字体的信息 """ - font_name: str = Fonts.鸿蒙简体 - font_size: int = 13 - bold: bool = False - italic: bool = False - stretch: bool = False - - def dict(self) -> Dict[str, Union[str, int, bool]]: - return dict(font_name=self.font_name, - font_size=self.font_size, - bold=self.bold, - italic=self.italic, - stretch=self.stretch) diff --git a/Difficult_Rocket/api/types/data_type.py b/Difficult_Rocket/api/types/data_type.py deleted file mode 100644 index 448d6ec..0000000 --- a/Difficult_Rocket/api/types/data_type.py +++ /dev/null @@ -1,30 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- - -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" - -# system function -import ctypes - -# Difficult_Rocket function - -# libs function -# from MCDR.serializer import Serializable - -""" -DR 内部数据传输格式类型 -""" - - -class datas(ctypes.Structure): - _fields_: list = [("name", ctypes.c_char_p), - ("uuid", ctypes.c_char_p), - ] diff --git a/Difficult_Rocket/client/__init__.py b/Difficult_Rocket/client/__init__.py index 8f4597e..1fa7b13 100644 --- a/Difficult_Rocket/client/__init__.py +++ b/Difficult_Rocket/client/__init__.py @@ -5,21 +5,15 @@ # All rights reserved # ------------------------------- -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" -import inspect -# system function import os +import sys import time import logging +import inspect import functools import traceback -from typing import List, Callable +from typing import Callable, Dict, List, TYPE_CHECKING from decimal import Decimal # third function @@ -32,6 +26,8 @@ from pyglet.window import Window from pyglet.window import key, mouse # Difficult_Rocket function +if TYPE_CHECKING: + from Difficult_Rocket.main import Game from Difficult_Rocket.utils import tools from Difficult_Rocket.api.types import Options from Difficult_Rocket.command import line, tree @@ -43,7 +39,6 @@ from Difficult_Rocket.client.fps.fps_log import FpsLogger from Difficult_Rocket.client.guis.widgets import InputBox from Difficult_Rocket.exception.command import CommandError from Difficult_Rocket.exception.language import LanguageNotFound -from Difficult_Rocket.client.render.sr1_ship import SR1ShipRender from Difficult_Rocket.client.screen import DRScreen, DRDEBUGScreen @@ -59,7 +54,7 @@ class ClientOption(Options): caption: str = "Difficult Rocket v{DR_version}|DR_rs v{DR_Rust_get_version}" def load_file(self) -> None: - file = tools.load_file('./configs/main.toml') + file: dict = tools.load_file('./configs/main.toml') self.fps = int(file['runtime']['fps']) self.width = int(file['window']['width']) self.height = int(file['window']['height']) @@ -70,7 +65,7 @@ class ClientOption(Options): class Client: - def __init__(self, net_mode='local'): + def __init__(self, game: "Game", net_mode='local'): start_time = time.time_ns() # logging self.logger = logging.getLogger('client') @@ -82,20 +77,14 @@ class Client: self.process_name = 'Client process' self.process_pid = os.getpid() self.net_mode = net_mode - file_drop = bool( - pyglet.compat_platform != 'darwin' or DR_option.pyglet_macosx_dev_test - ) - self.window = ClientWindow(net_mode=self.net_mode, width=self.config.width, height=self.config.height, + self.game = game + self.window = ClientWindow(game=game, net_mode=self.net_mode, + width=self.config.width, height=self.config.height, fullscreen=self.config.fullscreen, caption=self.config.caption, resizable=self.config.resizeable, visible=self.config.visible, - file_drops=file_drop) + file_drops=True) end_time = time.time_ns() self.use_time = end_time - start_time - if DR_option.use_DR_rust: - from libs.Difficult_Rocket_rs import read_ship_test, part_list_read_test - # part_list_read_test() - # read_ship_test() - self.logger.info(tr().client.setup.use_time().format(Decimal(self.use_time) / 1000000000)) self.logger.debug(tr().client.setup.use_time_ns().format(self.use_time)) @@ -119,7 +108,7 @@ def _call_screen_after(func: Callable) -> Callable: @functools.wraps(func) def warped(self: "ClientWindow", *args, **kwargs): result = func(self, *args, **kwargs) - for a_screen in self.screen_list: + for title, a_screen in self.screen_list.items(): a_screen.window_pointer = self # 提前帮子窗口设置好指针 if hasattr(a_screen, func.__name__): @@ -136,7 +125,7 @@ def _call_screen_after(func: Callable) -> Callable: def _call_screen_before(func: Callable) -> Callable: @functools.wraps(func) def warped(self: "ClientWindow", *args, **kwargs): - for a_screen in self.screen_list: + for title, a_screen in self.screen_list.items(): a_screen.window_pointer = self # 提前帮子窗口设置好指针 if hasattr(a_screen, func.__name__): @@ -153,7 +142,7 @@ def _call_screen_before(func: Callable) -> Callable: class ClientWindow(Window): - def __init__(self, net_mode='local', *args, **kwargs): + def __init__(self, game: "Game", net_mode='local', *args, **kwargs): """ @param net_mode: @@ -166,8 +155,10 @@ class ClientWindow(Window): self.logger = logging.getLogger('client') self.logger.info(tr().window.setup.start()) # value + self.game = game self.net_mode = net_mode self.run_input = False + self.command_list: List[str] = [] # configs self.main_config = tools.load_file('./configs/main.toml') self.game_config = tools.load_file('./configs/game.config') @@ -181,7 +172,7 @@ class ClientWindow(Window): # frame self.frame = pyglet.gui.Frame(self, order=20) self.M_frame = pyglet.gui.MovableFrame(self, modifier=key.LCTRL) - self.screen_list: List[BaseScreen] = [] + self.screen_list: Dict[str, BaseScreen] = {} # setup self.setup() # 命令显示 @@ -194,7 +185,7 @@ class ClientWindow(Window): self.set_handlers(self.input_box) self.input_box.enabled = True # 设置刷新率 - pyglet.clock.schedule_interval(self.draw_update, float(self.SPF)) + # pyglet.clock.schedule_interval(self.draw_update, float(self.SPF)) # 完成设置后的信息输出 self.logger.info(tr().window.os.pid_is().format(os.getpid(), os.getppid())) end_time = time.time_ns() @@ -208,9 +199,9 @@ class ClientWindow(Window): self.set_icon(pyglet.image.load('./textures/icon.png')) self.load_fonts() # TODO 读取配置文件,加载不同的屏幕,解耦 - self.screen_list.append(DRDEBUGScreen(self)) - self.screen_list.append(DRScreen(self)) - self.screen_list.append(SR1ShipRender(self)) + self.screen_list['DR_debug'] = DRDEBUGScreen(self) + self.screen_list['DR_main'] = DRScreen(self) + self.game.dispatch_event('on_client_start', game=self.game, client=self) def load_fonts(self) -> None: fonts_folder_path = self.main_config['runtime']['fonts_folder'] @@ -222,27 +213,35 @@ class ClientWindow(Window): self.set_icon(pyglet.image.load('./textures/icon.png')) self.run_input = True self.read_input() - pyglet.app.event_loop.run(1 / self.main_config['runtime']['fps']) + try: + pyglet.app.event_loop.run(1 / self.main_config['runtime']['fps']) + except KeyboardInterrupt: + print("==========client stop. KeyboardInterrupt info==========") + traceback.print_exc() + print("==========client stop. KeyboardInterrupt info end==========") + self.dispatch_event("on_close") + sys.exit(0) @new_thread('window read_input', daemon=True) def read_input(self): self.logger.debug('read_input start') while self.run_input: - get = input(">") + try: + get = input(">") + except (EOFError, KeyboardInterrupt): + self.run_input = False + break if get in ('', ' ', '\n', '\r'): continue if get == 'stop': self.run_input = False - try: - self.on_command(line.CommandText(get)) - except CommandError: - self.logger.error(traceback.format_exc()) + self.command_list.append(get) self.logger.debug('read_input end') @new_thread('window save_info') def save_info(self): self.logger.info(tr().client.config.save.start()) - config_file = tools.load_file('./configs/main.toml') + config_file: dict = tools.load_file('./configs/main.toml') config_file['window']['width'] = self.width config_file['window']['height'] = self.height config_file['runtime']['language'] = DR_runtime.language @@ -253,8 +252,8 @@ class ClientWindow(Window): client api """ - def add_sub_screen(self, sub_screen: BaseScreen.__class__): - self.screen_list.append(sub_screen(self)) + def add_sub_screen(self, title: str, sub_screen: type(BaseScreen)): + self.screen_list[title] = sub_screen(self) """ draws and some event @@ -268,9 +267,13 @@ class ClientWindow(Window): @_call_screen_after def on_draw(self, *dt): - # self.logger.debug('on_draw call dt: {}'.format(dt)) + if self.command_list: + for command in self.command_list: + self.on_command(line.CommandText(command)) + self.command_list.pop(0) pyglet.gl.glClearColor(0.1, 0, 0, 0.0) self.clear() + self.draw_update(float(self.SPF)) self.draw_batch() @_call_screen_after @@ -434,6 +437,7 @@ class ClientWindow(Window): @_call_screen_before def on_close(self, source: str = 'window') -> None: + self.game.dispatch_event('on_close', game=self.game, client=self, source=source) self.logger.info(tr().window.game.stop_get().format(tr().game[source]())) self.logger.info(tr().window.game.stop()) self.fps_log.check_list = False diff --git a/Difficult_Rocket/main.py b/Difficult_Rocket/main.py index 56152bc..1318e7b 100644 --- a/Difficult_Rocket/main.py +++ b/Difficult_Rocket/main.py @@ -15,17 +15,22 @@ import os import sys import time import logging +import importlib +import importlib.util import logging.config import multiprocessing from io import StringIO +from pathlib import Path +from typing import TYPE_CHECKING if __name__ == '__main__': # been start will not run this sys.path.append('/bin/libs') sys.path.append('/bin') -from Difficult_Rocket import client, server, DR_option - +from Difficult_Rocket import client, server, DR_option, DR_runtime +if TYPE_CHECKING: + from Difficult_Rocket.api.mod import ModInfo from Difficult_Rocket.crash import write_info_to_cache from Difficult_Rocket.utils import tools from Difficult_Rocket.utils.translate import tr @@ -58,6 +63,9 @@ class Game: # version check self.log_env() self.python_version_check() + self.loaded_mods = [] + # self.client = client.Client + # self.server = server.Server self.setup() def log_env(self) -> None: @@ -66,8 +74,67 @@ class Game: text = cache_steam.getvalue() self.logger.info(text) + def load_mods(self) -> None: + mods = [] + mod_path = Path(DR_runtime.mod_path) + if not mod_path.exists(): + self.logger.info(tr().main.mod.find.faild.no_mod_folder()) + return + # 寻找有效 mod + paths = mod_path.iterdir() + sys.path.append(DR_runtime.mod_path) + for mod_path in paths: + try: + if mod_path.name == '__pycache__': + continue + self.logger.info(tr().main.mod.find.start().format(mod_path)) + if mod_path.is_dir(): + if importlib.util.find_spec(mod_path.name) is not None: + mods.append(mod_path.name) + else: + self.logger.warning(tr().main.mod.load.faild.info().format(mod_path.name, tr().main.mod.find.faild.no_spec())) + elif mod_path.suffix in ('.pyz', '.zip', '.pyd', '.py'): + if importlib.util.find_spec(mod_path.name) is not None: + mods.append(mod_path.name) + except ImportError as e: + self.logger.warning(tr().main.mod.find.faild().format(mod_path, e)) + self.logger.info(tr().main.mod.find.done()) + # 加载有效 mod + module = [] + for mod in mods: + try: + self.logger.info(tr().main.mod.load.start().format(mod)) + mod_module = importlib.import_module(mod) + if not hasattr(mod_module, "mod_class"): + self.logger.warning(tr().main.mod.load.faild.info().format(mod, tr().main.mod.load.faild.no_mod_class())) + del mod_module # 释放内存 + continue + mod_class: type(ModInfo) = mod_module.mod_class + mod_class = mod_class() + module.append(mod_class) + self.logger.info(tr().main.mod.load.info().format(mod_class.mod_id, mod_class.version)) + except ImportError as e: + self.logger.warning(tr().main.mod.load.faild().format(mod, e)) + self.logger.info(tr().main.mod.load.done()) + self.loaded_mods = module + mod_list = [] + for mod in module: + mod_list.append((mod.mod_id, mod.version)) + # 调用 on_load + self.dispatch_event('on_load', game=self) + DR_runtime.DR_Mod_List = mod_list + + def dispatch_event(self, event_name: str, *args, **kwargs) -> None: + for mod in self.loaded_mods: + if hasattr(mod, event_name): + try: + getattr(mod, event_name)(*args, **kwargs) + except Exception as e: + self.logger.error(tr().main.mod.event.error().format(event_name, e, mod.mod_id)) + def setup(self) -> None: - self.client = client.Client(net_mode='local') + self.load_mods() + self.client = client.Client(game=self, net_mode='local') self.server = server.Server(net_mode='local') def python_version_check(self) -> None: # best 3.8+ and write at 3.8.10 diff --git a/Difficult_Rocket/mod/__init__.py b/Difficult_Rocket/mod/__init__.py index 757a792..95b52bb 100644 --- a/Difficult_Rocket/mod/__init__.py +++ b/Difficult_Rocket/mod/__init__.py @@ -4,68 +4,3 @@ # All rights reserved # ------------------------------- -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" - -# system function -from typing import Tuple, List - -# from libs -from MCDR.version import Version -from MCDR.serializer import Serializable - -# from DR -from Difficult_Rocket import DR_runtime, Options - -""" -mod系统参数 -""" -MOD_loader_version = "0.1.0.0" # mod系统版本 版本号遵守 semver ++ -semver_loader_version = Version(MOD_loader_version) - -""" -加载mod时会更改的参数 -这里的只是范例,实际加载时会根据mod配置修改 -""" - -RequireVersion = Tuple[Version, Version] -# 第一个是最低兼容版本,第二个是最高兼容版本 -# 例如: ("1.0.0", "1.1.0") 表示从1.0.0版本开始兼容,到1.1.0版本结束兼容 -ForceRequire = bool - - -# TODO 完善中 -class MODInfo(Serializable): - """ - 加载mod时候的参数 - """ - """基本信息""" - name: str # mod 名称 - version: Version # mod 版本 - - """作者、描述""" - writer: str # 作者 - link: str = "" # 作者链接 - description: str = "" # 描述 (务必简洁明了) - info: str = "" # 其他信息 (可以很多很多) - - """版本相关信息""" - DR_version: RequireVersion = (DR_runtime.DR_version, DR_runtime.DR_version) # DR SDK 兼容版本 - DR_Api_version: RequireVersion = (DR_runtime.API_version, DR_runtime.API_version) # DR Api版本 - Mod_Require_version: List[Tuple[str, ForceRequire, RequireVersion]] = [] # mod 依赖版本 - - """mod 状态""" - is_enable: bool = True # 是否启用 - is_loaded: bool = False # 是否加载 - - """mod 配置""" - config: Options = Options() # mod 配置存储 - - -""" -一些重置用函数 -""" diff --git a/Difficult_Rocket/mod/loader/__init__.py b/Difficult_Rocket/mod/loader/__init__.py index 8fcca51..6460ec0 100644 --- a/Difficult_Rocket/mod/loader/__init__.py +++ b/Difficult_Rocket/mod/loader/__init__.py @@ -4,9 +4,8 @@ # All rights reserved # ------------------------------- -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" +from Difficult_Rocket.api.screen import BaseScreen +from Difficult_Rocket import DR_option, DR_runtime + + + diff --git a/Difficult_Rocket/utils/__init__.py b/Difficult_Rocket/utils/__init__.py index 9a6bddf..95b52bb 100644 --- a/Difficult_Rocket/utils/__init__.py +++ b/Difficult_Rocket/utils/__init__.py @@ -4,17 +4,3 @@ # All rights reserved # ------------------------------- -""" -writen by shenjackyuanjie -mail: 3695888@qq.com -github: @shenjackyuanjie -gitee: @shenjackyuanjie -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .new_thread import new_thread - -__all__ = ['new_thread'] - diff --git a/Difficult_Rocket/utils/new_thread.py b/Difficult_Rocket/utils/new_thread.py index c9fb5a5..c76838a 100644 --- a/Difficult_Rocket/utils/new_thread.py +++ b/Difficult_Rocket/utils/new_thread.py @@ -7,8 +7,7 @@ import functools import inspect import threading -from Difficult_Rocket import crash, DR_option -from typing import Optional, Callable +from typing import Optional, Callable, Union, List """ This part of code come from MCDReforged(https://github.com/Fallen-Breath/MCDReforged) @@ -17,6 +16,15 @@ GNU Lesser General Public License v3.0(GNU LGPL v3) (have some changes) """ +__all__ = [ + 'new_thread', + 'FunctionThread' +] + + +record_thread = False +record_destination: List[Callable[['FunctionThread'], None]] = [] + def copy_signature(target: Callable, origin: Callable) -> Callable: """ @@ -28,10 +36,13 @@ def copy_signature(target: Callable, origin: Callable) -> Callable: class FunctionThread(threading.Thread): + """ + A Thread subclass which is used in decorator :func:`new_thread` to wrap a synchronized function call + """ __NONE = object() - def __init__(self, target, name, args, kwargs): - super().__init__(target=target, args=args, kwargs=kwargs, name=name) + def __init__(self, target, name, args, kwargs, daemon): + super().__init__(target=target, args=args, kwargs=kwargs, name=name, daemon=daemon) self.__return_value = self.__NONE self.__error = None @@ -40,12 +51,33 @@ class FunctionThread(threading.Thread): self.__return_value = target(*args_, **kwargs_) except Exception as e: self.__error = e - print(e) raise e from None self._target = wrapped_target def get_return_value(self, block: bool = False, timeout: Optional[float] = None): + """ + Get the return value of the original function + + If an exception has occurred during the original function call, the exception will be risen again here + + Examples:: + + >>> import time + >>> @new_thread + ... def do_something(text: str): + ... time.sleep(1) + ... return text + + >>> do_something('task').get_return_value(block=True) + 'task' + + :param block: If it should join the thread before getting the return value to make sure the function invocation finishes + :param timeout: The maximum timeout for the thread join + :raise RuntimeError: If the thread is still alive when getting return value. Might be caused by ``block=False`` + while the thread is still running, or thread join operation times out + :return: The return value of the original function + """ if block: self.join(timeout) if self.__return_value is self.__NONE: @@ -54,30 +86,57 @@ class FunctionThread(threading.Thread): raise self.__error return self.__return_value - def join(self, timeout: Optional[float] = None) -> None: - super().join(timeout) - def start(self) -> None: - super().start() - - -def new_thread(thread_name: Optional[str or Callable] = None, +def new_thread(arg: Optional[Union[str, Callable]] = None, daemon: bool = False, log_thread: bool = True): """ - Use a new thread to execute the decorated function - The function return value will be set to the thread instance that executes this function - The name of the thread can be specified in parameter + This is a one line solution to make your function executes in parallels. + When decorated with this decorator, functions will be executed in a new daemon thread + + This decorator only changes the return value of the function to the created ``Thread`` object. + Beside the return value, it reserves all signatures of the decorated function, + so you can safely use the decorated function as if there's no decorating at all + + It's also a simple compatible upgrade method for old MCDR 0.x plugins + + The return value of the decorated function is changed to the ``Thread`` object that executes this function + + The decorated function has 1 extra field: + + * ``original`` field: The original undecorated function + + Examples:: + + >>> import time + + >>> @new_thread('My Plugin Thread') + ... def do_something(text: str): + ... time.sleep(1) + ... print(threading.current_thread().name) + >>> callable(do_something.original) + True + >>> t = do_something('foo') + >>> isinstance(t, FunctionThread) + True + >>> t.join() + My Plugin Thread + + :param arg: A :class:`str`, the name of the thread. It's recommend to specify the thread name, so when you + log something by ``server.logger``, a meaningful thread name will be displayed + instead of a plain and meaningless ``Thread-3`` + :param daemon: If the thread should be a daemon thread + :param log_thread: If the thread should be logged to callback defined in record_destination """ def wrapper(func): @functools.wraps(func) # to preserve the origin function information def wrap(*args, **kwargs): - thread = FunctionThread(target=func, args=args, kwargs=kwargs, name=thread_name) - thread.daemon = daemon + thread = FunctionThread(target=func, args=args, kwargs=kwargs, name=thread_name, daemon=daemon) + if record_thread: + for destination in record_destination: + destination(thread) thread.start() - if log_thread and DR_option.record_threads: - crash.all_thread.append(thread) return thread # bring the signature of the func to the wrap function @@ -86,10 +145,11 @@ def new_thread(thread_name: Optional[str or Callable] = None, wrap.original = func # access this field to get the original function return wrap - # Directly use @on_new_thread without ending brackets case - if isinstance(thread_name, Callable): - this_is_a_function = thread_name + # Directly use @new_thread without ending brackets case, e.g. @new_thread + if isinstance(arg, Callable): thread_name = None - return wrapper(this_is_a_function) - # Use @on_new_thread with ending brackets case - return wrapper + return wrapper(arg) + # Use @new_thread with ending brackets case, e.g. @new_thread('A'), @new_thread() + else: + thread_name = arg + return wrapper diff --git a/Difficult_Rocket/utils/options.py b/Difficult_Rocket/utils/options.py new file mode 100644 index 0000000..f5bdfa6 --- /dev/null +++ b/Difficult_Rocket/utils/options.py @@ -0,0 +1,194 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import traceback +from dataclasses import dataclass +from typing import get_type_hints, Type, List, Union, Dict, Any, Callable, Tuple, Optional, TYPE_CHECKING + +__all__ = ['get_type_hints_', + 'Options', + 'Fonts', + 'FontData', + 'OptionsError', + 'OptionNotFound', + 'OptionNameNotDefined'] + + +def get_type_hints_(cls: Type): + try: + return get_type_hints(cls) + except ValueError: + return get_type_hints(cls, globalns={}) + + +class OptionsError(Exception): + """ option 的错误基类""" + + +class OptionNameNotDefined(OptionsError): + """ 向初始化的 option 里添加了一个不存在于选项里的选项 """ + + +class OptionNotFound(OptionsError): + """ 某个选项没有找到 """ + + +class Options: + """ + Difficult Rocket 的游戏配置的存储基类 + """ + name = 'Option Base' + cached_options: Dict[str, Union[str, Any]] = {} + + def __init__(self, **kwargs): + """ + 创建一个新的 Options 的时候的配置 + 如果存在 init 方法 会在设置完 kwargs 之后运行子类的 init 方法 + :param kwargs: + """ + if TYPE_CHECKING: + self.options: Dict[str, Union[Callable, object]] = {} + self.flush_option() + for option, value in kwargs.items(): + if option not in self.cached_options: + raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined") + setattr(self, option, value) + if hasattr(self, 'init'): + self.init(**kwargs) + if hasattr(self, 'load_file'): + try: + self.load_file() + except Exception: + traceback.print_exc() + self.flush_option() + + if TYPE_CHECKING: + options: Dict[str, Union[Callable, object]] = {} + + def init(self, **kwargs) -> None: + """ 如果子类定义了这个函数,则会在 __init__ 之后调用这个函数 """ + + def load_file(self) -> bool: + """如果子类定义了这个函数,则会在 __init__ 和 init 之后再调用这个函数 + + 请注意,这个函数请尽量使用 try 包裹住可能出现错误的部分 + 否则会在控制台输出你的报错""" + return True + + def option(self) -> Dict[str, Any]: + """ + 获取配置类的所有配置 + :return: 自己的所有配置 + """ + values = {} + for ann in self.__annotations__: # 获取类型注释 + values[ann] = getattr(self, ann, None) + if values[ann] is None: + values[ann] = self.__annotations__[ann] + + if not hasattr(self, 'options'): + self.options: Dict[str, Union[Callable, object]] = {} + for option, a_fun in self.options.items(): # 获取额外内容 + values[option] = a_fun + + for option, a_fun in values.items(): # 检查是否为 property + if a_fun is bool and getattr(self, option, None) is not None: + values[option] = False + if isinstance(a_fun, property): + try: + values[option] = getattr(self, option) + except AttributeError: + raise OptionNotFound(f'Option {option} is not found in {self.name}') from None + return values + + def format(self, text: str) -> str: + """ + 使用自己的选项给输入的字符串替换内容 + :param text: 想替换的内容 + :return: 替换之后的内容 + """ + cache_option = self.flush_option() + for option, value in cache_option.items(): + text = text.replace(f'{{{option}}}', str(value)) + return text + + def flush_option(self) -> Dict[str, Any]: + """ + 刷新缓存 options 的内容 + :return: 刷新过的 options + """ + self.cached_options = self.option() + return self.cached_options + + def option_with_len(self) -> List[Union[List[Tuple[str, Any, Any]], int, Any]]: + options = self.flush_option() + max_len_key = 1 + max_len_value = 1 + max_len_value_t = 1 + option_list = [] + for key, value in options.items(): + value_t = value if isinstance(value, Type) else type(value) + max_len_key = max(max_len_key, len(key)) + max_len_value = max(max_len_value, len(str(value))) + max_len_value_t = max(max_len_value_t, len(str(value_t))) + option_list.append((key, value, value_t)) + return [option_list, max_len_key, max_len_value, max_len_value_t] + + @classmethod + def add_option(cls, name: str, value: Union[Callable, object]) -> Dict: + if not hasattr(cls, 'options'): + cls.options: Dict[str, Union[Callable, object]] = {} + cls.options[name] = value + return cls.options + + @staticmethod + def init_option(options_class: Type['Options'], init_value: Optional[dict] = None) -> 'Options': + return options_class(**init_value if init_value is not None else {}) + + +class Fonts(Options): + # font's value + + HOS: str = 'HarmonyOS Sans' + HOS_S: str = 'HarmonyOS Sans SC' + HOS_T: str = 'HarmonyOS Sans TC' + HOS_C: str = 'HarmonyOS Sans Condensed' + + 鸿蒙字体: str = HOS + 鸿蒙简体: str = HOS_S + 鸿蒙繁体: str = HOS_T + 鸿蒙窄体: str = HOS_C + + CC: str = 'Cascadia Code' + CM: str = 'Cascadia Mono' + CCPL: str = 'Cascadia Code PL' + CMPL: str = 'Cascadia Mono PL' + + 微软等宽: str = CC + 微软等宽无线: str = CM + 微软等宽带电线: str = CCPL + 微软等宽带电线无线: str = CMPL + + 得意黑: str = '得意黑' + # SS = smiley-sans + SS: str = 得意黑 + + +@dataclass +class FontData: + """ 用于保存字体的信息 """ + font_name: str = Fonts.鸿蒙简体 + font_size: int = 13 + bold: bool = False + italic: bool = False + stretch: bool = False + + def dict(self) -> Dict[str, Union[str, int, bool]]: + return dict(font_name=self.font_name, + font_size=self.font_size, + bold=self.bold, + italic=self.italic, + stretch=self.stretch) diff --git a/Difficult_Rocket/utils/translate.py b/Difficult_Rocket/utils/translate.py index 86c04e5..63f5de1 100644 --- a/Difficult_Rocket/utils/translate.py +++ b/Difficult_Rocket/utils/translate.py @@ -121,8 +121,8 @@ class Translates: self._config.is_final = True return self - def __call__(self, *args, **kwargs) -> Union[dict, list, int, str]: - return self.__str__() + def __call__(self, *args, **kwargs) -> str: + return str(self) def copy(self): return self.__copy__() diff --git a/biggit.ps1 b/biggit.ps1 new file mode 100644 index 0000000..70c643d --- /dev/null +++ b/biggit.ps1 @@ -0,0 +1,20 @@ + +$objects = git verify-pack -v .git/objects/pack/pack-aba4bfc55979194c86dbd466c86e57d8199ae7ad.idx | Select-String -Pattern 'chain' -NotMatch | ForEach-Object {$_.Line} | Sort-Object -Property @{Expression={$_.Split(" ")[2]}; Ascending=$false} | Select-Object -First 50 + +Write-Output "All sizes are in kB. The pack column is the size of the object, compressed, inside the pack file." + +$output = "size,pack,SHA,location" +foreach ($y in $objects) { + # extract the size in bytes + $size = [int]($y.Split(" ")[4]/1024) + # extract the compressed size in bytes + $compressedSize = [int]($y.Split(" ")[5]/1024) + # extract the SHA + $sha = $y.Split(" ")[0] + # find the objects location in the repository tree + $other = git rev-list --all --objects | Select-String $sha + $output += "`n${size},${compressedSize},${other}" +} + +Write-Output $output | ConvertFrom-Csv -Delimiter "," | Format-Table +Pause diff --git a/build_rs.ps1 b/build_rs.ps1 index 2ce11e2..b77858a 100644 --- a/build_rs.ps1 +++ b/build_rs.ps1 @@ -1,4 +1,4 @@ -Set-Location .\libs\Difficult_Rocket_rs\src +Set-Location .\mods\dr_game\Difficult_Rocket_rs\src Write-Output $args[0] @@ -23,6 +23,6 @@ if ($do -or "311" -in $args) { python3.11 setup.py build } -python3 post_build.py +python3.8 post_build.py -Set-Location ..\..\..\ +Set-Location ..\..\..\..\ diff --git a/configs/game.config b/configs/game.config deleted file mode 100644 index 509b2d1..0000000 --- a/configs/game.config +++ /dev/null @@ -1,4 +0,0 @@ -[command] -log = 1000 -show = 20 -size = 12 diff --git a/configs/lang/en-us.toml b/configs/lang/en-us.toml index 10e9b5e..2076d18 100644 --- a/configs/lang/en-us.toml +++ b/configs/lang/en-us.toml @@ -20,7 +20,16 @@ logger.logfile_name = "Log file name : " logger.logfile_level = "Log file record level : " logger.logfile_fmt = "Log file record format : " logger.logfile_datefmt = "Log file date format : " -game_start.at = "The main thread of the game starts with : " +game_start.at = "Game MainThread start at: {}" +mod.find.start = "Checking Mod: {}" +mod.find.faild.no_spec = "importlib can't find spec" +mod.find.faild.no_mod_folder = "Can't find mod folder" +mod.find.done = "All Mod checked" +mod.load.start = "Loading Mod: {}" +mod.load.info = "mod id: {} version: {}" +mod.load.faild.info = "Mod load failed: {} error info: {}" +mod.load.faild.no_mod_class = "Can't find Mod class" +mod.load.done = "All Mod loaded" [client] setup.start = "Client start loading" diff --git a/configs/lang/zh-CN.toml b/configs/lang/zh-CN.toml index 07abd4d..c9d93d3 100644 --- a/configs/lang/zh-CN.toml +++ b/configs/lang/zh-CN.toml @@ -21,6 +21,16 @@ logger.logfile_level = "日志文件记录级别:" logger.logfile_fmt = "日志文件记录格式:" logger.logfile_datefmt = "日志文件日期格式:" game_start.at = "游戏主线程开始于:" +mod.find.start = "正在校验 Mod: {}" +mod.find.faild.no_spec = "importlib 无法找到 spec" +mod.find.faild.no_mod_folder = "没有找到 Mod 文件夹" +mod.find.done = "所有 Mod 校验完成" +mod.load.start = "正在加载 Mod: {}" +mod.load.info = "mod id: {} 版本号: {}" +mod.load.faild.info = "Mod 加载失败: {} 错误信息: {}" +mod.load.faild.no_mod_class = "没有找到 Mod 类" +mod.load.done = "所有 Mod 加载完成" +mod.event.error = "Mod 事件 {} 发生错误 {} Mod: {}" [client] setup.start = "客户端加载开始" diff --git a/configs/main.toml b/configs/main.toml index 8e5b488..05599b8 100644 --- a/configs/main.toml +++ b/configs/main.toml @@ -7,8 +7,8 @@ fonts_folder = "libs/fonts" [window] style = "None" -width = 1406 -height = 900 +width = 2571 +height = 1510 visible = true gui_scale = 1 caption = "Difficult Rocket v{DR_version}|DR_rs v{DR_Rust_get_version}" @@ -19,3 +19,5 @@ full_screen = false width = 1024 height = 768 gui_scale = 1 +[game.mods] +path = "mods" diff --git a/docs/src/update_logs.md b/docs/src/update_logs.md index b1a4044..0ff3874 100644 --- a/docs/src/update_logs.md +++ b/docs/src/update_logs.md @@ -22,15 +22,54 @@ - [![Readme-gitee](https://img.shields.io/badge/Readme-中文(点我!)-blue.svg?style=flat-square)](../../README.md) - Using [SemVer 2.0.0](https://semver.org/) to manage version -## 202305 DR `0.8.0.0` + DR_api `0.1.0.0` +## 202305 DR `0.8.0.0` + DR_api `0.1.0.0` + DR_rs `0.2.7.0` + 15 > 啊哈! mod 加载来啦! +> 啊啊啊啊啊 大重构 api + +### DR_api `0.1.0.0` + +- 大概是一个可用的版本了 +- `ModInfo` + - `on_load(game: Game, old_self: Optional[ModInfo]) -> bool` + - `game`: Game 对象 用于存储 DR SDK 的信息 + - `old_self`: 旧的 ModInfo 对象, 可以用于从上次加载中恢复信息 + - 返回值: 是否加载成功 + - `on_client_start(game: Game, client: ClientWindow) -> None` + - `game`: Game 对象 用于存储 DR SDK 的信息 + - `client`: ClientWindow 对象 用于传递客户端状态 + - `on_client_stop(game: Game, client: ClientWindow, source: str = 'window')` + - `game`: Game 对象 用于存储 DR SDK 的信息 + - `client`: ClientWindow 对象 用于传递客户端状态 + - `source`: 关闭调用的来源 + +### DR_rs `0.2.7.0` + +- `__init__.py` + - 添加了 `SR1Ship_rs` 的 typing + - `name` + - `description` + - `lift_off` + - `touch_ground` + - `img_pos() -> Tuple[int, int, int, int]` +- 导出了 `SR1Ship_rs` + - Exported `SR1Ship_rs` +- `types::SR1PartData` + - `get_box(&self, part_type: &SR1PartType) -> (f64, f64, f64, f64)` +- `types::SR1Ship` + - `from_file` + ### Remove - `game.config` - 已删除 - Removed +- `DR_option` & `DR_runtime`(`long_version` `15`) + - 完全移除 `DR_rust` 部分 + - Completely removed the `DR_rust` part +- 现在 `client` 不会在 `setup()` 中调用 `DR_runtime` 的 `find_mods()` 方法 + - Now `client` will not call the `find_mods()` method of `DR_runtime` in `setup()` ### Changes @@ -53,6 +92,14 @@ - 现在游戏崩溃时会自动在 stdio 中输出崩溃日志 内容跟 crash report 中的基本相同 - Now when the game crashes, it will automatically output the crash log in stdio - The content of the crash log is basically the same as the crash report +- `utils.new_thread` + - 跟随 MCDR 的更新 + - 将记录线程的方式改成 函数回调 + - Follow the update of MCDR + - Change the way to record threads to function callbacks +- `Difficult_Rocket.api` + - 大重构,移除定义,改为引用 + - Big refactoring, remove definition, change to reference ### Docs @@ -67,6 +114,13 @@ ### Mod Loader - `ModInfo` + - `on_load(game: Game, old_self: Optional[ModInfo]) -> bool` + - `game`: Game 对象 用于存储 DR SDK 的信息 + - `old_self`: 旧的 ModInfo 对象, 可以用于从上次加载中恢复信息 + - 返回值: 是否加载成功 + - `game`: Game object used to store information about the DR SDK + - `old_self`: Old ModInfo object, can be used to restore information from the last load + - Return value: Whether the load is successful ## 20230422 DR `0.7.2.2` + DR_rs `0.2.6.1` + DR_api `0.0.2.0` + 14 @@ -147,6 +201,13 @@ - 修改了中文语言的翻译,在中英混杂的中间加入空格 - Modified the Chinese translation, and added a space in the middle of Chinese and English +### Fix + +- `mods` 路径不存在时 游戏会崩溃的 bug + - Bug that the game will crash when the `mods` path does not exist +- 游戏崩溃 / 从控制台退出时 `WerFault.exe` 会跳出来卡住程序的 bug + - Bug that `WerFault.exe` will pop up and block the program when the game crashes / exits from the console + ## 20230405 DR `0.7.2.1` ### Changes diff --git a/libs/Difficult_Rocket_rs/lib/Difficult_Rocket_rs.cp38-win_amd64.pyd b/libs/Difficult_Rocket_rs/lib/Difficult_Rocket_rs.cp38-win_amd64.pyd deleted file mode 100644 index 87e3213..0000000 --- a/libs/Difficult_Rocket_rs/lib/Difficult_Rocket_rs.cp38-win_amd64.pyd +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b19a52878b8290578d90aba8cea77fcceb9d064fa266ef0b1bdc514802a6f8b -size 1477632 diff --git a/libs/pyglet/canvas/win32.py b/libs/pyglet/canvas/win32.py index 52cdf52..31c08bb 100644 --- a/libs/pyglet/canvas/win32.py +++ b/libs/pyglet/canvas/win32.py @@ -3,7 +3,6 @@ from .base import Display, Screen, ScreenMode, Canvas from pyglet.libs.win32 import _user32 from pyglet.libs.win32.constants import * from pyglet.libs.win32.types import * -from pyglet.libs.win32.context_managers import device_context class Win32Display(Display): @@ -31,13 +30,13 @@ class Win32Screen(Screen): self._handle = handle def get_matching_configs(self, template): - with device_context(None) as hdc: - canvas = Win32Canvas(self.display, 0, hdc) - configs = template.match(canvas) - # XXX deprecate config's being screen-specific - for config in configs: - config.screen = self - + hdc = _user32.GetDC(0) + canvas = Win32Canvas(self.display, 0, hdc) + configs = template.match(canvas) + # XXX deprecate config's being screen-specific + for config in configs: + config.screen = self + _user32.ReleaseDC(0, hdc) return configs def get_device_name(self): diff --git a/libs/pyglet/font/win32.py b/libs/pyglet/font/win32.py index e4b696e..be7b225 100644 --- a/libs/pyglet/font/win32.py +++ b/libs/pyglet/font/win32.py @@ -9,7 +9,6 @@ from pyglet.font import base from pyglet.font import win32query import pyglet.image from pyglet.libs.win32.constants import * -from pyglet.libs.win32.context_managers import device_context from pyglet.libs.win32.types import * from pyglet.libs.win32 import _gdi32 as gdi32, _user32 as user32 from pyglet.libs.win32 import _kernel32 as kernel32 @@ -196,13 +195,14 @@ class Win32Font(base.Font): self.hfont = gdi32.CreateFontIndirectW(byref(self.logfont)) # Create a dummy DC for coordinate mapping - with device_context(None) as dc: - metrics = TEXTMETRIC() - gdi32.SelectObject(dc, self.hfont) - gdi32.GetTextMetricsA(dc, byref(metrics)) - self.ascent = metrics.tmAscent - self.descent = -metrics.tmDescent - self.max_glyph_width = metrics.tmMaxCharWidth + dc = user32.GetDC(0) + metrics = TEXTMETRIC() + gdi32.SelectObject(dc, self.hfont) + gdi32.GetTextMetricsA(dc, byref(metrics)) + self.ascent = metrics.tmAscent + self.descent = -metrics.tmDescent + self.max_glyph_width = metrics.tmMaxCharWidth + user32.ReleaseDC(0, dc) def __del__(self): gdi32.DeleteObject(self.hfont) @@ -210,22 +210,22 @@ class Win32Font(base.Font): @staticmethod def get_logfont(name, size, bold, italic, dpi): # Create a dummy DC for coordinate mapping - with device_context(None) as dc: - if dpi is None: - dpi = 96 - logpixelsy = dpi - - logfont = LOGFONTW() - # Conversion of point size to device pixels - logfont.lfHeight = int(-size * logpixelsy // 72) - if bold: - logfont.lfWeight = FW_BOLD - else: - logfont.lfWeight = FW_NORMAL - logfont.lfItalic = italic - logfont.lfFaceName = name - logfont.lfQuality = ANTIALIASED_QUALITY + dc = user32.GetDC(0) + if dpi is None: + dpi = 96 + logpixelsy = dpi + logfont = LOGFONTW() + # Conversion of point size to device pixels + logfont.lfHeight = int(-size * logpixelsy // 72) + if bold: + logfont.lfWeight = FW_BOLD + else: + logfont.lfWeight = FW_NORMAL + logfont.lfItalic = italic + logfont.lfFaceName = name + logfont.lfQuality = ANTIALIASED_QUALITY + user32.ReleaseDC(0, dc) return logfont @classmethod diff --git a/libs/pyglet/font/win32query.py b/libs/pyglet/font/win32query.py index 91b101b..ace9975 100644 --- a/libs/pyglet/font/win32query.py +++ b/libs/pyglet/font/win32query.py @@ -71,7 +71,6 @@ appropriate typeface name and create the font using CreateFont or CreateFontIndirect. """ -from pyglet.libs.win32.context_managers import device_context DEBUG = False @@ -386,33 +385,36 @@ def query(charset=DEFAULT_CHARSET): global FONTDB # 1. Get device context of the entire screen - with device_context(None) as hdc: + hdc = user32.GetDC(None) - # 2. Call EnumFontFamiliesExA (ANSI version) + # 2. Call EnumFontFamiliesExA (ANSI version) - # 2a. Call with empty font name to query all available fonts - # (or fonts for the specified charset) - # - # NOTES: - # - # * there are fonts that don't support ANSI charset - # * for DEFAULT_CHARSET font is passed to callback function as - # many times as charsets it supports + # 2a. Call with empty font name to query all available fonts + # (or fonts for the specified charset) + # + # NOTES: + # + # * there are fonts that don't support ANSI charset + # * for DEFAULT_CHARSET font is passed to callback function as + # many times as charsets it supports - # [ ] font name should be less than 32 symbols with terminating \0 - # [ ] check double purpose - enumerate all available font names - # - enumerate all available charsets for a single font - # - other params? + # [ ] font name should be less than 32 symbols with terminating \0 + # [ ] check double purpose - enumerate all available font names + # - enumerate all available charsets for a single font + # - other params? - logfont = LOGFONTW(0, 0, 0, 0, 0, 0, 0, 0, charset, 0, 0, 0, 0, '') - FONTDB = [] # clear cached FONTDB for enum_font_names callback - res = gdi32.EnumFontFamiliesExW( - hdc, # handle to device context - ctypes.byref(logfont), - enum_font_names, # pointer to callback function - 0, # lParam - application-supplied data - 0) # dwFlags - reserved = 0 - # res here is the last value returned by callback function + logfont = LOGFONTW(0, 0, 0, 0, 0, 0, 0, 0, charset, 0, 0, 0, 0, '') + FONTDB = [] # clear cached FONTDB for enum_font_names callback + res = gdi32.EnumFontFamiliesExW( + hdc, # handle to device context + ctypes.byref(logfont), + enum_font_names, # pointer to callback function + 0, # lParam - application-supplied data + 0) # dwFlags - reserved = 0 + # res here is the last value returned by callback function + + # 3. Release DC + user32.ReleaseDC(None, hdc) return FONTDB diff --git a/libs/pyglet/libs/win32/context_managers.py b/libs/pyglet/libs/win32/context_managers.py deleted file mode 100644 index 9deec64..0000000 --- a/libs/pyglet/libs/win32/context_managers.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Win32 resources as handy context managers. - -These are intended to help keep loader code clean & stable at the price -of a tiny bit of execution speed. Performance-critical code should avoid -using these helpers in favor of direct win32 API calls. - -Before:: - - def loader_function(arg): - context_handle = user32.GetResource(None) - - result = calculation(arg) - - # Easily forgotten! - user32.ReleaseResource(context_handle) - - return result - -After:: - - def loader_function(arg): - - with resource_context() as context_handle: - result = calculation(arg) - - return result - -""" -from contextlib import contextmanager -from typing import Optional, Generator -from ctypes.wintypes import HANDLE -from ctypes import WinError -from pyglet.libs.win32 import _user32 as user32 - - -@contextmanager -def device_context(window_handle: Optional[int] = None) -> Generator[HANDLE, None, None]: - """ - A Windows device context wrapped as a context manager. - - Args: - window_handle: A specific window handle to use, if any. - Raises: - WinError: Raises if a device context cannot be acquired or released - Yields: - HANDLE: the managed drawing context handle to auto-close. - - """ - if not (_dc := user32.GetDC(window_handle)): - raise WinError() - - try: - yield _dc - finally: - if not user32.ReleaseDC(None, _dc): - raise WinError() diff --git a/mods/builtin/__init__.py b/mods/builtin/__init__.py deleted file mode 100644 index e65b6e9..0000000 --- a/mods/builtin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- diff --git a/libs/Difficult_Rocket_rs/__init__.py b/mods/dr_game/Difficult_Rocket_rs/__init__.py similarity index 84% rename from libs/Difficult_Rocket_rs/__init__.py rename to mods/dr_game/Difficult_Rocket_rs/__init__.py index 03c57ef..8b601e1 100644 --- a/libs/Difficult_Rocket_rs/__init__.py +++ b/mods/dr_game/Difficult_Rocket_rs/__init__.py @@ -40,22 +40,16 @@ if TYPE_CHECKING: @property def dx(self) -> float: ... - @property def dy(self) -> float: ... - @property def zoom(self) -> float: ... - @property def position(self) -> Tuple[float, float]: ... - @dx.setter def dx(self, value: float) -> None: ... - @dy.setter def dy(self, value: float) -> None: ... - @zoom.setter def zoom(self, value: float) -> None: ... @@ -93,3 +87,17 @@ if TYPE_CHECKING: def as_dict(self) -> Dict[str, SR1PartType_rs]: ... def get_part_type(self, name: str) -> SR1PartType_rs: ... + + class SR1Ship_rs: + """ 用于高效且省内存的读取 SR1Ship """ + @property + def name(self) -> str: ... + @property + def description(self) -> str: ... + @property + def lift_off(self) -> bool: ... + @property + def touch_ground(self) -> bool: ... + @property + def img_pos(self) -> Tuple[int, int, int, int]: ... + """ -x -y +x +y 左下右上 """ diff --git a/libs/Difficult_Rocket_rs/src/.gitignore b/mods/dr_game/Difficult_Rocket_rs/src/.gitignore similarity index 100% rename from libs/Difficult_Rocket_rs/src/.gitignore rename to mods/dr_game/Difficult_Rocket_rs/src/.gitignore diff --git a/libs/Difficult_Rocket_rs/src/Cargo.lock b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock similarity index 100% rename from libs/Difficult_Rocket_rs/src/Cargo.lock rename to mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock diff --git a/libs/Difficult_Rocket_rs/src/Cargo.toml b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml similarity index 100% rename from libs/Difficult_Rocket_rs/src/Cargo.toml rename to mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml diff --git a/libs/Difficult_Rocket_rs/src/mac-build.ps1 b/mods/dr_game/Difficult_Rocket_rs/src/mac-build.ps1 similarity index 100% rename from libs/Difficult_Rocket_rs/src/mac-build.ps1 rename to mods/dr_game/Difficult_Rocket_rs/src/mac-build.ps1 diff --git a/libs/Difficult_Rocket_rs/src/post_build.py b/mods/dr_game/Difficult_Rocket_rs/src/post_build.py similarity index 100% rename from libs/Difficult_Rocket_rs/src/post_build.py rename to mods/dr_game/Difficult_Rocket_rs/src/post_build.py diff --git a/libs/Difficult_Rocket_rs/src/pyproject.toml b/mods/dr_game/Difficult_Rocket_rs/src/pyproject.toml similarity index 100% rename from libs/Difficult_Rocket_rs/src/pyproject.toml rename to mods/dr_game/Difficult_Rocket_rs/src/pyproject.toml diff --git a/libs/Difficult_Rocket_rs/src/readme.md b/mods/dr_game/Difficult_Rocket_rs/src/readme.md similarity index 100% rename from libs/Difficult_Rocket_rs/src/readme.md rename to mods/dr_game/Difficult_Rocket_rs/src/readme.md diff --git a/libs/Difficult_Rocket_rs/src/setup.py b/mods/dr_game/Difficult_Rocket_rs/src/setup.py similarity index 80% rename from libs/Difficult_Rocket_rs/src/setup.py rename to mods/dr_game/Difficult_Rocket_rs/src/setup.py index b3c4657..ff43eda 100644 --- a/libs/Difficult_Rocket_rs/src/setup.py +++ b/mods/dr_game/Difficult_Rocket_rs/src/setup.py @@ -3,22 +3,16 @@ # Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com # All rights reserved # ------------------------------- -import os import sys import shutil from setuptools import setup from setuptools_rust import Binding, RustExtension -sys.path.append('../../../') -sys.path.append(os.curdir) -if '../../../' in sys.path: - from Difficult_Rocket import DR_runtime - package_path = 'Difficult_Rocket_rs' setup( name='Difficult_Rocket_rs', - version=DR_runtime.DR_Rust_version.__str__(), + version="0.2.7.0", author='shenjackyuanjie', author_email='3695888@qq.com', rust_extensions=[RustExtension(target="Difficult_Rocket_rs.Difficult_Rocket_rs", diff --git a/libs/Difficult_Rocket_rs/src/src/__init__.py b/mods/dr_game/Difficult_Rocket_rs/src/src/__init__.py similarity index 100% rename from libs/Difficult_Rocket_rs/src/src/__init__.py rename to mods/dr_game/Difficult_Rocket_rs/src/src/__init__.py diff --git a/libs/Difficult_Rocket_rs/src/src/lib.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/lib.rs similarity index 92% rename from libs/Difficult_Rocket_rs/src/src/lib.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/lib.rs index 9b447cf..cb41705 100644 --- a/libs/Difficult_Rocket_rs/src/src/lib.rs +++ b/mods/dr_game/Difficult_Rocket_rs/src/src/lib.rs @@ -17,7 +17,7 @@ mod types; use pyo3::prelude::*; #[pyfunction] -fn get_version_str() -> String { "0.2.6.1".to_string() } +fn get_version_str() -> String { "0.2.7.0".to_string() } #[pyfunction] fn test_call(py_obj: &PyAny) -> PyResult { @@ -39,6 +39,7 @@ fn module_init(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/libs/Difficult_Rocket_rs/src/src/logger.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/logger.rs similarity index 100% rename from libs/Difficult_Rocket_rs/src/src/logger.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/logger.rs diff --git a/libs/Difficult_Rocket_rs/src/src/plugin.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/plugin.rs similarity index 93% rename from libs/Difficult_Rocket_rs/src/src/plugin.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/plugin.rs index 4eebdf6..9796efe 100644 --- a/libs/Difficult_Rocket_rs/src/src/plugin.rs +++ b/mods/dr_game/Difficult_Rocket_rs/src/src/plugin.rs @@ -15,11 +15,10 @@ pub mod plugin_trait { pub struct ModInfo { pub name: String, - pub version: String + pub version: String, } pub trait ModInfoTrait { fn info() -> ModInfo; } - } diff --git a/libs/Difficult_Rocket_rs/src/src/python.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/python.rs similarity index 78% rename from libs/Difficult_Rocket_rs/src/src/python.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/python.rs index 6453fe8..329320a 100644 --- a/libs/Difficult_Rocket_rs/src/src/python.rs +++ b/mods/dr_game/Difficult_Rocket_rs/src/src/python.rs @@ -13,8 +13,8 @@ pub mod data { use crate::sr1_data::part_list::RawPartList; use crate::sr1_data::ship::RawShip; + use crate::types::sr1::{SR1PartData, SR1PartListTrait}; use crate::types::sr1::{SR1PartList, SR1PartType, SR1Ship}; - use crate::types::sr1::{SR1PartListTrait, SR1ShipTrait}; #[pyclass] #[pyo3(name = "SR1PartType_rs")] @@ -70,6 +70,12 @@ pub mod data { } } + #[pyclass] + #[pyo3(name = "SR1PartData_rs")] + pub struct PySR1PartData { + pub data: SR1PartData, + } + #[pyclass] #[pyo3(name = "SR1Ship_rs")] #[pyo3(text_signature = "(file_path = './configs/dock1.xml', part_list = './configs/PartList.xml', ship_name = 'NewShip')")] @@ -82,11 +88,29 @@ pub mod data { impl PySR1Ship { #[new] fn new(file_path: String, part_list: String, ship_name: String) -> Self { - let raw_ship: RawShip = RawShip::from_file(file_path).unwrap(); - let ship = raw_ship.to_sr_ship(Some(ship_name)); + let ship = SR1Ship::from_file(file_path, Some(ship_name)).unwrap(); let part_list = SR1PartList::from_file(part_list).unwrap(); Self { ship, part_list } } + + fn get_img_pos(&self) -> (i64, i64, i64, i64) { + let mut img_pos = (0, 0, 0, 0); + // -x, -y, +x, +y + // 左下角,右上角 + for part in self.ship.types.iter() { + // let part_box = part + todo!("get_img_pos") + } + img_pos + } + + fn get_name(&self) -> String { self.ship.name.clone() } + + fn get_description(&self) -> String { self.ship.description.clone() } + + fn get_lift_off(&self) -> bool { self.ship.lift_off } + + fn get_touch_ground(&self) -> bool { self.ship.touch_ground } } } @@ -94,6 +118,12 @@ pub mod translate { use pyo3::prelude::*; use pyo3::types::PyDict; + #[derive(Clone)] + pub enum BoolString { + Bool(bool), + String(String), + } + #[pyclass] #[pyo3(name = "TranslateConfig_rs")] #[pyo3(text_signature = "(language, raise_error = False, replace_normal = False, add_error = False, is_result = False, keep_get = False)")] @@ -121,6 +151,13 @@ pub mod translate { language: language.unwrap_or(default_language), } } + + // fn set(&self, py_: Python, item: String, value: BoolString) -> &Self { + // match item.as_str() { + // "raise_error" => self, + // _ => self, + // } + // } } #[pyclass] diff --git a/libs/Difficult_Rocket_rs/src/src/render.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/render.rs similarity index 100% rename from libs/Difficult_Rocket_rs/src/src/render.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/render.rs diff --git a/libs/Difficult_Rocket_rs/src/src/rustfmt.toml b/mods/dr_game/Difficult_Rocket_rs/src/src/rustfmt.toml similarity index 100% rename from libs/Difficult_Rocket_rs/src/src/rustfmt.toml rename to mods/dr_game/Difficult_Rocket_rs/src/src/rustfmt.toml diff --git a/libs/Difficult_Rocket_rs/src/src/simulator.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/simulator.rs similarity index 97% rename from libs/Difficult_Rocket_rs/src/src/simulator.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/simulator.rs index 01afd11..01262f3 100644 --- a/libs/Difficult_Rocket_rs/src/src/simulator.rs +++ b/mods/dr_game/Difficult_Rocket_rs/src/src/simulator.rs @@ -9,10 +9,9 @@ use pyo3::prelude::*; use rapier2d_f64::prelude::*; - #[pyfunction] #[pyo3(name = "simluation")] -pub fn simluation() -> PyResult<()> { +pub fn simluation() -> () { let mut rigid_body_set = RigidBodySet::new(); let mut collider_set = ColliderSet::new(); @@ -62,6 +61,4 @@ pub fn simluation() -> PyResult<()> { let ball_body = &rigid_body_set[ball_body_handle]; println!("Ball altitude: {} {}", ball_body.translation().x, ball_body.translation().y); } - Ok(()) } - diff --git a/libs/Difficult_Rocket_rs/src/src/sr1_data.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/sr1_data.rs similarity index 100% rename from libs/Difficult_Rocket_rs/src/src/sr1_data.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/sr1_data.rs diff --git a/libs/Difficult_Rocket_rs/src/src/types.rs b/mods/dr_game/Difficult_Rocket_rs/src/src/types.rs similarity index 87% rename from libs/Difficult_Rocket_rs/src/src/types.rs rename to mods/dr_game/Difficult_Rocket_rs/src/src/types.rs index df6c174..e5d7369 100644 --- a/libs/Difficult_Rocket_rs/src/src/types.rs +++ b/mods/dr_game/Difficult_Rocket_rs/src/src/types.rs @@ -8,7 +8,9 @@ pub mod sr1 { use std::collections::HashMap; + use std::fs; + use super::math::{Edge, Shape}; use crate::sr1_data::part_list::Damage as RawDamage; use crate::sr1_data::part_list::{AttachPoint, AttachPoints, Engine, Lander, Rcs, Shape as RawShape, Solar, Tank}; use crate::sr1_data::part_list::{RawPartList, RawPartType, SR1PartTypeEnum}; @@ -179,31 +181,49 @@ pub mod sr1 { #[derive(Debug, Clone)] pub struct SR1PartList { pub types: Vec, - pub cache: Option>, + pub cache: HashMap, pub name: String, } impl SR1PartList { + #[inline] + pub fn new(name: String, types: Vec) -> SR1PartList { + let mut map = HashMap::new(); + for part in types.iter() { + map.insert(part.id.clone(), part.clone()); + } + SR1PartList { + types, + cache: map, + name, + } + } + #[inline] pub fn from_file(file_name: String) -> Option { if let Some(raw_list) = RawPartList::from_file(file_name) { - return Some(raw_list.to_sr_part_list(None)); + let sr_list = raw_list.to_sr_part_list(None); + let mut map = HashMap::new(); + for part in sr_list.types.iter() { + map.insert(part.id.clone(), part.clone()); + } } None } #[inline] - pub fn get_hash_map(&mut self) -> HashMap { - if let Some(map) = &self.cache { - return map.clone(); + pub fn get_part_type(self, type_name: String) -> Option { + if let Some(part) = self.cache.get(&type_name) { + return Some(part.clone()); } - let mut map = HashMap::new(); - for part in self.types.iter() { - map.insert(part.id.clone(), part.clone()); - } - self.cache = Some(map.clone()); - map + None } + + pub fn part_types_new(part_types: Vec, name: Option) -> Self { + SR1PartList::new(name.unwrap_or("NewPartList".to_string()), part_types) + } + + pub fn insert_part(&mut self, part: SR1PartType) -> () { self.types.insert(0, part); } } pub trait SR1PartTypeData { @@ -226,17 +246,6 @@ pub mod sr1 { fn to_raw_ship(&self) -> RawShip; } - impl SR1PartList { - #[inline] - pub fn new(name: String, types: Vec) -> Self { SR1PartList { name, cache: None, types } } - - pub fn part_types_new(part_types: Vec, name: Option) -> Self { - SR1PartList::new(name.unwrap_or("NewPartList".to_string()), part_types) - } - - pub fn insert_part(&mut self, part: SR1PartType) -> () { self.types.insert(0, part); } - } - impl SR1PartListTrait for SR1PartList { fn to_sr_part_list(&self, name: Option) -> SR1PartList { return if let Some(name) = name { @@ -491,6 +500,32 @@ pub mod sr1 { pub explode: bool, } + impl SR1PartData { + pub fn get_box(&self, part_type: &SR1PartType) -> (f64, f64, f64, f64) { + let width = part_type.width; + let height = part_type.height; + let radius = self.angle; + let mut shape = Shape::new_width_height(width as f64, height as f64, Some(radius)); + shape.move_xy(Some(self.x), Some(self.y)); + let mut pos_box = (0_f64, 0_f64, 0_f64, 0_f64); + match shape.bounds[0] { + Edge::OneTimeLine(line) => { + pos_box.0 = line.start.x; + pos_box.1 = line.start.y; + } + _ => {} + } + match shape.bounds[2] { + Edge::OneTimeLine(line) => { + pos_box.2 = line.start.x; + pos_box.3 = line.start.y; + } + _ => {} + } + pos_box + } + } + #[derive(Debug, Clone)] pub enum SR1PartDataAttr { Tank { @@ -532,6 +567,18 @@ pub mod sr1 { pub disconnected: Option, Option>)>>, } + impl SR1Ship { + pub fn from_file(file_name: String, ship_name: Option) -> Option { + // 首先验证文件是否存在 不存在则返回None + if !std::path::Path::new(&file_name).exists() { + return None; + } + // 解析为 RawShip + let ship: RawShip = RawShip::from_file(file_name).unwrap(); + Some(ship.to_sr_ship(ship_name)) + } + } + impl SR1ShipTrait for SR1Ship { #[inline] fn to_sr_ship(&self, name: Option) -> SR1Ship { @@ -582,6 +629,14 @@ pub mod sr1 { } } } + + pub fn get_max_box(parts: &Vec, part_list: &SR1PartList) -> (f64, f64, f64, f64) { + let mut max_box = (0_f64, 0_f64, 0_f64, 0_f64); + for part in parts.iter() { + let part_type = part_list.get_part_type(part.part_type); + } + todo!("get_max_box") + } } #[allow(unused)] @@ -686,7 +741,7 @@ pub mod math { } } - pub fn new_width_height(width: f64, height: f64, angle: Option) -> Self { + pub fn new_width_height(width: f64, height: f64, radius: Option) -> Self { let d_width = width / 2.0; let d_height = height / 2.0; let mut edges: Vec = vec![ @@ -695,17 +750,17 @@ pub mod math { Edge::OneTimeLine(OneTimeLine::pos_new(d_width, d_height, -d_width, d_height)), Edge::OneTimeLine(OneTimeLine::pos_new(-d_width, d_height, -d_width, -d_height)), ]; - if let Some(angle) = angle { + if let Some(radius) = radius { edges = edges .iter() .map(|edge| match edge { Edge::OneTimeLine(line) => { - let start = line.start.rotate(angle); - let end = line.end.rotate(angle); + let start = line.start.rotate_radius(radius); + let end = line.end.rotate_radius(radius); Edge::OneTimeLine(OneTimeLine::point_new(&start, &end)) } Edge::CircularArc(arc) => { - let pos = arc.pos.rotate(angle); + let pos = arc.pos.rotate_radius(radius); Edge::CircularArc(CircularArc { r: arc.r, pos, @@ -722,6 +777,25 @@ pub mod math { bounds: edges, } } + + pub fn move_xy(&mut self, x: Option, y: Option) { + let x = x.unwrap_or(0.0); + let y = y.unwrap_or(0.0); + for edge in self.bounds.iter() { + match edge { + Edge::OneTimeLine(mut line) => { + line.start.x += x; + line.start.y += y; + line.end.x += x; + line.end.y += y; + } + Edge::CircularArc(mut arc) => { + arc.pos.x += x; + arc.pos.y += y; + } + } + } + } } impl OneTimeLine { diff --git a/mods/dr_game/__init__.py b/mods/dr_game/__init__.py new file mode 100644 index 0000000..112642c --- /dev/null +++ b/mods/dr_game/__init__.py @@ -0,0 +1,84 @@ +# ------------------------------- +# Difficult Rocket Mod +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import warnings +import traceback + +from typing import Optional + + +from libs.MCDR.version import Version +from Difficult_Rocket.main import Game +from Difficult_Rocket.api.mod import ModInfo +from Difficult_Rocket.api.types import Options +from Difficult_Rocket.client import ClientWindow + +DR_rust_version = Version("0.2.6.2") # DR_mod 的 Rust 编写部分的兼容版本 + + +class _DR_mod_runtime(Options): + + use_DR_rust: bool = True + DR_rust_available: bool = False + DR_rust_version: Version = DR_rust_version + DR_rust_get_version: Optional[Version] = None + + def init(self) -> None: + try: + from .Difficult_Rocket_rs import get_version_str + self.DR_rust_get_version = Version(get_version_str()) + self.DR_rust_available = True + if self.DR_rust_get_version != self.DR_rust_version: + relationship = 'larger' if self.DR_rust_version > self.DR_rust_version else 'smaller' + warnings.warn(f'DR_rust builtin version is {self.DR_rust_version} but true version is {get_version_str()}.\n' + f'Builtin version {relationship} than true version') + self.use_DR_rust = self.use_DR_rust and self.DR_rust_available + except Exception: + traceback.print_exc() + self.DR_rust_available = False + self.use_DR_rust = False + self.flush_option() + + +DR_mod_runtime = _DR_mod_runtime() + + +class DR_mod(ModInfo): + + mod_id = "difficult_rocket_mod" + name = "Difficult Rocket mod" + version = Version("0.7.2.2") + + writer = "shenjackyuanjie" + link = "shenjack.top" + description = "Difficult Rocket mod (where the game implement)" + info = "Difficult Rocket mod (where the game implement)" + + config = DR_mod_runtime + + # DR_version = # DR SDK 兼容版本 + # 反正是内置 mod 跟着最新版本的 DR 走就行了 + # DR_Api_version = # DR Api版本 + # 同理 不管 API 版本 这东西要是不兼容了才是大问题 + + def on_load(self, game: Game, old_self: Optional["DR_mod"] = None) -> bool: + if not DR_mod_runtime.DR_rust_available: + return False + if old_self: + game.client.window.add_sub_screen("SR1_ship", old_self.screen) + else: + self.config.flush_option() + print("DR_mod: on_load") + return True + + def on_client_start(self, game: Game, client: ClientWindow): + from .sr1_ship import SR1ShipRender + self.screen = SR1ShipRender + print('DR_mod: on_client_start') + client.add_sub_screen("SR1_ship", SR1ShipRender) + + +mod_class = DR_mod diff --git a/Difficult_Rocket/client/render/sr1_ship.py b/mods/dr_game/sr1_ship.py similarity index 96% rename from Difficult_Rocket/client/render/sr1_ship.py rename to mods/dr_game/sr1_ship.py index d60fd5d..6a46b49 100644 --- a/Difficult_Rocket/client/render/sr1_ship.py +++ b/mods/dr_game/sr1_ship.py @@ -19,8 +19,10 @@ from pyglet.math import Vec4 from pyglet.text import Label from pyglet.shapes import Line from pyglet.sprite import Sprite +from pyglet.image import Texture from pyglet.graphics import Batch, Group -from pyglet.image import load as load_image + +from . import DR_mod_runtime # Difficult Rocket from Difficult_Rocket import DR_option @@ -33,8 +35,8 @@ from Difficult_Rocket.api.types.SR1 import SR1Textures, SR1PartTexture, SR1PartD if TYPE_CHECKING: from Difficult_Rocket.client import ClientWindow -if DR_option.use_DR_rust: - from libs.Difficult_Rocket_rs import CenterCamera_rs, SR1PartList_rs +if DR_mod_runtime.use_DR_rust: + from .Difficult_Rocket_rs import CenterCamera_rs, SR1PartList_rs logger = logging.getLogger('client') @@ -128,7 +130,7 @@ class SR1ShipRender(BaseScreen): load_end_time = time.time_ns() logger.info(tr().client.sr1_render.setup.use_time().format( (load_end_time - load_start_time) / 1000000000)) - if DR_option.use_DR_rust: + if DR_mod_runtime.use_DR_rust: self.camera_rs = CenterCamera_rs(main_window, min_zoom=(1 / 2) ** 10, max_zoom=10) self.rust_parts = None @@ -194,7 +196,7 @@ class SR1ShipRender(BaseScreen): self.part_data: Dict[int, SR1PartData] = {} self.parts_sprite: Dict[int, Sprite] = {} self.camera_rs.zoom = 1.0 - if DR_option.use_DR_rust: + if DR_mod_runtime.use_DR_rust: self.camera_rs.dx = 0 self.camera_rs.dy = 0 parts = self.xml_root.find('Parts') @@ -213,13 +215,13 @@ class SR1ShipRender(BaseScreen): self.drawing = False self.need_draw = False full_mass = 0 - if DR_option.use_DR_rust: + if DR_mod_runtime.use_DR_rust: for part in self.part_data: full_mass += self.part_list_rs.get_part_type(self.part_data[part].p_type).mass * 500 logger.info(tr().client.sr1_render.ship.load_time().format( (time.perf_counter_ns() - start_time) / 1000000000)) logger.info(tr().client.sr1_render.ship.info().format( - len(self.part_data), f'{full_mass}kg' if DR_option.use_DR_rust else tr().game.require_DR_rs())) + len(self.part_data), f'{full_mass}kg' if DR_mod_runtime.use_DR_rust else tr().game.require_DR_rs())) self.rendered = True def get_ship_size(self) -> (int, int): @@ -359,6 +361,12 @@ class SR1ShipRender(BaseScreen): image_data = screenshot(self.window_pointer) image_data.save('test.png') + elif command.re_match('gen_img'): + if not self.rendered: + return + # ship_size = self.ship.size + base_textures = Texture.create(100, 100) + ... def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int, window: "ClientWindow"): if not self.focus: