diff --git a/DR.py b/DR.py index 3aacbc7..09a5ec4 100644 --- a/DR.py +++ b/DR.py @@ -52,8 +52,9 @@ def start(start_time_ns: int) -> None: print(crash.all_process) for a_thread in threading.enumerate(): print(a_thread) - if a_thread.is_alive() and a_thread != threading.current_thread() and a_thread != threading.main_thread(): - a_thread.join(2) # wait for 2 sec + if a_thread.is_alive() and not a_thread.daemon: + if a_thread != threading.current_thread() and a_thread != threading.main_thread(): + a_thread.join(2) # wait for 2 sec import pyglet pyglet.app.exit() # make sure that pyglet has stopped diff --git a/config/main.toml b/config/main.toml index fc01c1a..eecdf90 100644 --- a/config/main.toml +++ b/config/main.toml @@ -7,8 +7,8 @@ fonts_folder = "assets/fonts" [window] style = "None" -width = 1112 -height = 793 +width = 1312 +height = 915 visible = true gui_scale = 1 caption = "Difficult Rocket v{DR_version}" diff --git a/docs/src/change_log/dr_game.md b/docs/src/change_log/dr_game.md index 77493c1..d35ecbf 100644 --- a/docs/src/change_log/dr_game.md +++ b/docs/src/change_log/dr_game.md @@ -1,9 +1,17 @@ - # DR game/DR rs 更新日志 - 最新版本号 - DR game: 0.3.3.0 - - DR rs: 0.2.22.0 + - DR rs: 0.2.23.0 + +## 20231101 DR rs 0.2.23.0 + +### Dependency + +- Update `DR rs` dependency + - `quick-xml`: `0.30.0` -> `0.31.0` + - `serde`: `1.0.186` -> `1.0.190` + - `xml-rs`: `0.8.16` -> `0.8.19` ## 20230825 DR rs 0.2.22.0 diff --git a/docs/src/change_log/dr_sdk.md b/docs/src/change_log/dr_sdk.md index 0a20318..cfe7eeb 100644 --- a/docs/src/change_log/dr_sdk.md +++ b/docs/src/change_log/dr_sdk.md @@ -1,4 +1,3 @@ - # DR SDK 更新日志 - 最新版本号 @@ -14,6 +13,20 @@ - 不再同时维护两份代码 - No longer maintain two sets of code at the same time +### Fix + +- 如果没有 DR_game 的情况下, 退出时会 join 控制台线程 + - 通过检测线程是否是守护线程来判断是否 join + - If there is no DR_game, join the console thread when exiting + - Determine whether to join by detecting whether the thread is a daemon thread + +### Dependency + +- 更新了所有的依赖版本号 + - 去看 `requirements.txt` 吧 +- Updated all dependency version numbers + - Go see `requirements.txt` + ## DR sdk 0.8.7.2 ### Add diff --git a/libs/lib_not_dr/__init__.py b/libs/lib_not_dr/__init__.py index c0958cc..9852726 100644 --- a/libs/lib_not_dr/__init__.py +++ b/libs/lib_not_dr/__init__.py @@ -4,4 +4,4 @@ # All rights reserved # ------------------------------- -__version__ = '0.1.7' +__version__ = '0.1.8' diff --git a/libs/lib_not_dr/logger/__init__.py b/libs/lib_not_dr/logger/__init__.py new file mode 100644 index 0000000..a94671b --- /dev/null +++ b/libs/lib_not_dr/logger/__init__.py @@ -0,0 +1,33 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- +import sys + +from lib_not_dr.types.options import Options + +COLOR_SUPPORT = True + +if sys.platform == "win32": + try: + # https://stackoverflow.com/questions/36760127/... + # how-to-use-the-new-support-for-ansi-escape-sequences-in-the-windows-10-console + from ctypes import windll + + kernel32 = windll.kernel32 + kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) + except OSError: # pragma: no cover + COLOR_SUPPORT = False + + +class LogLevel(Options): + name = 'LogLevel' + notset: int = 0 + trace: int = 5 + fine: int = 7 + debug: int = 10 + info: int = 20 + warn: int = 30 + error: int = 40 + fatal: int = 50 diff --git a/libs/lib_not_dr/logger/formatter/__init__.py b/libs/lib_not_dr/logger/formatter/__init__.py new file mode 100644 index 0000000..3f444db --- /dev/null +++ b/libs/lib_not_dr/logger/formatter/__init__.py @@ -0,0 +1,285 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- +import time + +from pathlib import Path +from string import Template +from typing import List, Union, Optional, Dict, Tuple, TYPE_CHECKING + +from lib_not_dr.logger import LogLevel +from lib_not_dr.types.options import Options +from lib_not_dr.logger.structure import LogMessage, FormattingMessage + +if TYPE_CHECKING: + from lib_not_dr.logger.formatter.colors import BaseColorFormatter + + +class BaseFormatter(Options): + name = 'BaseFormatter' + + sub_formatter: List['BaseFormatter'] = [] + color_formatters: List['BaseColorFormatter'] = [] + default_template: str = '[${log_time}][${level}]|${logger_name}:${logger_tag}|${messages}' + + @classmethod + def add_info(cls, match: str, to: str, description: str) -> str: + return f'- {to} -> ${{{match}}} : {description}' + + @classmethod + def info(cls) -> str: + infos = {BaseFormatter.name: BaseFormatter._info()} + cache = '' + for formatter in cls.sub_formatter: + infos[formatter.name] = formatter._info() + infos[cls.name] = cls._info() + for name, info in infos.items(): + cache += f"## {name}\n" + cache += info + cache += '\n' + return cache + + @classmethod + def _info(cls) -> str: + info = cls.add_info('logger_name', 'logger name', 'The name of the logger') + info += '\n' + info += cls.add_info('logger_tag', 'logger tag', 'The tag of the logger') + return info + + def format_message(self, + message: LogMessage, + template: Optional[Union[Template, str]] = None) -> str: + """ + Format message + :param message: 输入的消息 + :param template: 日志输出模板 + :return: + """ + basic_info = message.format_for_message() + message, info = self._format((message, basic_info)) + + if template is None: + template = Template(self.default_template) + elif isinstance(template, str): + template = Template(template) + + try: + return template.substitute(**info) + except (KeyError, ValueError): + return template.safe_substitute(**info) + + def _format(self, message: FormattingMessage) -> FormattingMessage: + """ + Format message + :param message: + :return: + """ + for formatter in self.sub_formatter: + message = formatter._format(message) + return message + + @property + def template(self) -> str: + return self.default_template + + @template.setter + def template(self, template: str) -> None: + if not isinstance(template, str): + raise TypeError(f'The template must be str, not {type(template)}') + self.default_template = template + + +class LevelFormatter(BaseFormatter): + name = 'LevelFormatter' + + default_level: int = 20 + + # If True, the undefined level will be set to the higher nearest level. + level_get_higher: bool = True + + level_name_map = { + LogLevel.notset: 'NOTSET', + LogLevel.trace: ' TRACE', + LogLevel.fine: ' FINE ', + LogLevel.debug: ' DEBUG', + LogLevel.info: ' INFO ', + LogLevel.warn: ' WARN ', + LogLevel.error: 'ERROR ', + LogLevel.fatal: 'FATAL ', + } + name_level_map = { + 'NOTSET': LogLevel.notset, + ' TRACE': LogLevel.trace, + ' FINE ': LogLevel.fine, + ' DEBUG': LogLevel.debug, + ' INFO ': LogLevel.info, + ' WARN ': LogLevel.warn, + 'ERROR ': LogLevel.error, + 'FATAL ': LogLevel.fatal, + } + + @classmethod + def _info(cls) -> str: + return cls.add_info('level', 'log level', 'The log level') + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if message[0].level in self.name_level_map: + level_tag = self.level_name_map[message[0].level] + else: + if self.level_get_higher: + for level in self.name_level_map: + if message[0].level <= self.name_level_map[level]: + level_tag = level + break + else: + level_tag = 'FATAL' + else: + for level in self.name_level_map: + if message[0].level >= self.name_level_map[level]: + level_tag = level + break + else: + level_tag = 'NOTSET' + message[1]['level'] = level_tag + return message + + +class TraceFormatter(BaseFormatter): + name = 'TraceFormatter' + + time_format: str = '%Y-%m-%d %H:%M:%S' + msec_time_format: str = '{}-{:03d}' + use_absolute_path: bool = False + + @classmethod + def _info(cls) -> str: + info = cls.add_info('log_time', 'formatted time when logging', 'The time format string' + '. See https://docs.python.org/3/library/time' + '.html#time.strftime for more information.') + info += '\n' + info += cls.add_info('log_source', 'logging file', 'the logging file name') + info += '\n' + info += cls.add_info('log_line', 'logging line', 'the logging line number') + info += '\n' + info += cls.add_info('log_function', 'logging function', 'the logging function name') + return info + + def _format(self, message: FormattingMessage) -> FormattingMessage: + message = self._time_format(message) + message = self._trace_format(message) + return message + + def _time_format(self, message: FormattingMessage) -> FormattingMessage: + time_mark = time.localtime(message[0].log_time / 1000000000) + if self.msec_time_format: + time_mark = self.msec_time_format.format(time.strftime(self.time_format, time_mark), + message[0].create_msec_3) + message[1]['log_time'] = time_mark + return message + + def _trace_format(self, message: FormattingMessage) -> FormattingMessage: + if message[0].stack_trace is None: + return message + path = Path(message[0].stack_trace.f_code.co_filename) + if self.use_absolute_path: + message[1]['log_source'] = path.absolute() + message[1]['log_source'] = path + message[1]['log_line'] = message[0].stack_trace.f_lineno + message[1]['log_function'] = message[0].stack_trace.f_code.co_name + return message + + +class StdFormatter(BaseFormatter): + name = 'StdFormatter' + + enable_color: bool = True + + sub_formatter: List[BaseFormatter] = [LevelFormatter(), + TraceFormatter()] + from lib_not_dr.logger.formatter.colors import (LevelColorFormatter, + LoggerColorFormatter, + TimeColorFormatter, + TraceColorFormatter, + MessageColorFormatter) + color_formatters: List[BaseFormatter] = [LevelColorFormatter(), + LoggerColorFormatter(), + TimeColorFormatter(), + TraceColorFormatter(), + MessageColorFormatter()] + + def __init__(self, + enable_color: bool = True, + sub_formatter: Optional[List[BaseFormatter]] = None, + color_formatters: Optional[List[BaseFormatter]] = None, + **kwargs) -> None: + """ + Initialize the StdFormatter + :param enable_color: enable color + :param sub_formatter: list of sub formatter + :param color_formatters: list of color formatter + :param kwargs: other options + """ + # 同 structures.LogMessage.__init__ 的注释 (逃) + self.enable_color = enable_color + if sub_formatter is not None: + self.sub_formatter = sub_formatter + if color_formatters is not None: + self.color_formatters = color_formatters + super().__init__(**kwargs) + + def _format(self, message: FormattingMessage) -> FormattingMessage: + super()._format(message) + + if not self.enable_color: + return message + + for formatter in self.color_formatters: + message = formatter._format(message) + + return message + + @classmethod + def _info(cls) -> str: + return 'None' + + +if __name__ == '__main__': + import inspect + + log_message = LogMessage(messages=['Hello World!'], + level=7, + stack_trace=inspect.currentframe(), + logger_tag='tester', + logger_name='test') + + print(LevelFormatter.info()) + print(LevelFormatter().format_message(log_message)) + + print(TraceFormatter.info()) + print(TraceFormatter().format_message(log_message)) + + print(StdFormatter.info()) + print(StdFormatter().format_message(log_message)) + + std_format = StdFormatter() + std_format.default_template = "${log_time}|${logger_name}|${logger_tag}|${log_source}:${log_line}|${log_function}|${level}|${messages}" + + test_levels = (0, 5, 7, 10, 20, 30, 40, 50) + + print("with color") + + for test_level in test_levels: + log_message.level = test_level + print(std_format.format_message(log_message), end='') + + print("without color") + + std_format.enable_color = False + + for test_level in test_levels: + log_message.level = test_level + print(std_format.format_message(log_message), end='') + + print(std_format.as_markdown()) diff --git a/libs/lib_not_dr/logger/formatter/colors.py b/libs/lib_not_dr/logger/formatter/colors.py new file mode 100644 index 0000000..49996bc --- /dev/null +++ b/libs/lib_not_dr/logger/formatter/colors.py @@ -0,0 +1,256 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +from lib_not_dr.logger import LogLevel, COLOR_SUPPORT +from lib_not_dr.logger.formatter import BaseFormatter +from lib_not_dr.logger.structure import FormattingMessage + +__all__ = [ + 'BaseColorFormatter', + + 'LevelColorFormatter', + 'LoggerColorFormatter', + 'TimeColorFormatter', + 'TraceColorFormatter', + 'MessageColorFormatter', + + 'RESET_COLOR' +] + +RESET_COLOR = '\033[0m' + + +class BaseColorFormatter(BaseFormatter): + name = 'BaseColorFormatter' + # TODO 迁移老 logger 颜色 + color = { + # Notset: just black + LogLevel.notset: '', + # Trace: blue + LogLevel.trace: '\033[38;2;138;173;244m', + # Fine: green + LogLevel.fine: '\033[0;32m', + # Debug: cyan + LogLevel.debug: '\033[0;36m', + # Info: white + LogLevel.info: '\033[0;37m', + # Warn: yellow + LogLevel.warn: '\033[0;33m', + # Error: red + LogLevel.error: '\033[0;31m', + # Fatal: red background + LogLevel.fatal: '\033[0;41m' + } + + def get_color(self, message: FormattingMessage) -> str: + for level in self.color: + if message[0].level <= level: + break + else: + level = 90 + return self.color[level] + + +class LevelColorFormatter(BaseColorFormatter): + name = 'LevelColorFormatter' + # TODO 迁移老 logger 颜色 + color = { + # Notset: just black + LogLevel.notset: '', + # Trace: blue + LogLevel.trace: '\033[38;2;138;173;244m', + # Fine: green + LogLevel.fine: '\033[35;48;2;44;44;54m', + # Debug: cyan + LogLevel.debug: '\033[38;2;133;138;149m', + # Info: white + LogLevel.info: '\033[0;37m', + # Warn: yellow + LogLevel.warn: '\033[0;33m', + # Error: red + LogLevel.error: '\033[0;31m', + # Fatal: red background + LogLevel.fatal: '\033[0;41m' + } + + @classmethod + def _info(cls) -> str: + return cls.add_info('colored level', 'level', 'A colored level') + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if isinstance(message[1].get('level'), int) or not COLOR_SUPPORT: + return message + # 获取颜色 + color = self.get_color(message) + # 添加颜色 + if color == '' or color == RESET_COLOR: + return message + message[1]['level'] = f'{color}{message[1]["level"]}{RESET_COLOR}' + return message + + +class LoggerColorFormatter(BaseColorFormatter): + name = 'LoggerColorFormatter' + # TODO 迁移老 logger 颜色 + color = { + # Notset: just black + LogLevel.notset: '', + # Trace: blue + LogLevel.trace: '\033[38;2;138;173;244m', + # Fine: green + LogLevel.fine: '\033[0;32m', + # Debug: cyan + LogLevel.debug: '\033[0;36m', + # Info: white + LogLevel.info: '\033[0;37m', + # Warn: yellow + LogLevel.warn: '\033[0;33m', + # Error: red + LogLevel.error: '\033[0;31m', + # Fatal: red background + LogLevel.fatal: '\033[38;2;245;189;230m', + } + + @classmethod + def _info(cls) -> str: + return cls.add_info('colored logger name', 'logger name', 'A colored logger name') + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if message[1].get('logger_name') is None or not COLOR_SUPPORT: + return message + # 获取颜色 + color = self.get_color(message) + # 添加颜色 + if color == '' or color == RESET_COLOR: + return message + message[1]['logger_name'] = f'{color}{message[1]["logger_name"]}{RESET_COLOR}' + if message[1].get('logger_tag') is not None and message[1].get('logger_tag') != ' ': + message[1]['logger_tag'] = f'{color}{message[1]["logger_tag"]}{RESET_COLOR}' + return message + + +class TimeColorFormatter(BaseColorFormatter): + name = 'TimeColorFormatter' + # TODO 迁移老 logger 颜色 + color = { + # Notset: just black + LogLevel.notset: '', + # Trace: blue + LogLevel.trace: '\033[38;2;138;173;244m', + # Fine: green + LogLevel.fine: '\033[0;32m', + # Debug: cyan + LogLevel.debug: '\033[0;36m', + # Info: white + LogLevel.info: '\033[0;37m', + # Warn: yellow + LogLevel.warn: '\033[0;33m', + # Error: red + LogLevel.error: '\033[0;31m', + # Fatal: red background + LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m', + } + + @classmethod + def _info(cls) -> str: + return cls.add_info('colored time', 'time', 'A colored time') + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if message[1].get('log_time') is None or not COLOR_SUPPORT: + return message + # 获取颜色 + color = self.get_color(message) + # 添加颜色 + if color == '' or color == RESET_COLOR: + return message + message[1]['log_time'] = f'{color}{message[1]["log_time"]}{RESET_COLOR}' + return message + + +class TraceColorFormatter(BaseColorFormatter): + name = 'TraceColorFormatter' + # TODO 迁移老 logger 颜色 + color = { + # Notset: just black + LogLevel.notset: '\033[38;2;0;255;180m', + # Trace: blue + LogLevel.trace: '\033[38;2;0;255;180m', + # Fine: green + LogLevel.fine: '\033[38;2;0;255;180m', + # Debug: cyan + LogLevel.debug: '\033[38;2;0;255;180m', + # Info: white + LogLevel.info: '\033[38;2;0;255;180m', + # Warn: yellow + LogLevel.warn: '\033[38;2;0;255;180m', + # Error: red + LogLevel.error: '\033[38;2;0;255;180m', + # Fatal: red background + LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m', + } + + @classmethod + def _info(cls) -> str: + info = cls.add_info('colored logging file', 'log_source', 'A colored logging file name') + info += '\n' + info += cls.add_info('colored logging line', 'log_line', 'A colored logging line number') + info += '\n' + info += cls.add_info('colored logging function', 'log_function', 'A colored logging function name') + return info + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if message[0].stack_trace is None or not COLOR_SUPPORT: + return message + # 获取颜色 + color = self.get_color(message) + # 添加颜色 + if color == '' or color == RESET_COLOR: + return message + message[1]['log_source'] = f'{color}{message[1]["log_source"]}{RESET_COLOR}' + message[1]['log_line'] = f'{color}{message[1]["log_line"]}{RESET_COLOR}' + message[1]['log_function'] = f'{color}{message[1]["log_function"]}{RESET_COLOR}' + return message + + +class MessageColorFormatter(BaseColorFormatter): + name = 'MessageColorFormatter' + + color = { + # Notset: just black + LogLevel.notset: '', + # Trace: blue + LogLevel.trace: '\033[38;2;138;173;244m', + # Fine: blue + LogLevel.fine: '\033[38;2;138;173;244m', + # Debug: blue + LogLevel.debug: '\033[38;2;138;173;244m', + # Info: no color + LogLevel.info: '', + # Warn: yellow + LogLevel.warn: '\033[0;33m', + # Error: red + LogLevel.error: '\033[0;31m', + # Fatal: red background + LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m', + } + + @classmethod + def _info(cls) -> str: + return cls.add_info('colored message', 'message', 'A colored message') + + def _format(self, message: FormattingMessage) -> FormattingMessage: + if message[1].get('messages') is None or not COLOR_SUPPORT: + return message + # 获取颜色 + color = self.get_color(message) + # 添加颜色 + if color == '' or color == RESET_COLOR: + return message + if message[1]['messages'][-1] == '\n': + message[1]['messages'] = f'{color}{message[1]["messages"][:-1]}{RESET_COLOR}\n' + else: + message[1]['messages'] = f'{color}{message[1]["messages"]}{RESET_COLOR}' + return message diff --git a/libs/lib_not_dr/logger/logger.py b/libs/lib_not_dr/logger/logger.py new file mode 100644 index 0000000..020edbc --- /dev/null +++ b/libs/lib_not_dr/logger/logger.py @@ -0,0 +1,267 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import time +import inspect +from types import FrameType +from typing import List, Optional + +from lib_not_dr.logger import LogLevel +from lib_not_dr.types.options import Options +from lib_not_dr.logger.structure import LogMessage +from lib_not_dr.logger.outstream import BaseOutputStream, StdioOutputStream + + +class Logger(Options): + name = 'Logger-v2' + + outputs: List[BaseOutputStream] = [StdioOutputStream()] + + log_name: str = 'root' + + enable: bool = True + level: int = 20 # info + + def log_for(self, level: int) -> bool: + """ + Check if logging is enabled for a specific level. + + Args: + level (int): The logging level to check. + + Returns: + bool: True if logging is enabled for the given level, False otherwise. + """ + return self.enable and level >= self.level + + def add_output(self, output: BaseOutputStream) -> None: + """ + Add an output to the list of outputs. + + Args: + output (BaseOutputStream): The output to be added. + + Returns: + None + """ + self.outputs.append(output) + self.level = min(self.level, output.level) + + def remove_output(self, output: BaseOutputStream) -> None: + """ + Removes the specified output from the list of outputs. + + Args: + output (BaseOutputStream): The output to be removed. + + Returns: + None + """ + self.outputs.remove(output) + self.level = max(self.level, *[output.level for output in self.outputs]) + + @property + def global_level(self) -> int: + """ + Get the global logging level. + + Returns: + int: The global logging level. + """ + return self.level + + @global_level.setter + def global_level(self, level: int) -> None: + """ + Set the global logging level. + + Args: + level (int): The global logging level. + + Returns: + None + """ + self.level = level + for output in self.outputs: + output.level = level + + def make_log(self, + messages: List[str], + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + level: int = 20, # info + # log_time: Optional[float] = None, + # logger_name: str = 'root', + # logger_tag: Optional[str] = None, + stack_trace: Optional[FrameType] = None) -> None: + # 检查是否需要记录 + if not self.log_for(level): + return + log_time = time.time_ns() + # 处理堆栈信息 + if stack_trace is None: + # 尝试获取堆栈信息 + if (stack := inspect.currentframe()) is not None: + # 如果可能 尝试获取上两层的堆栈信息 + if (up_stack := stack.f_back) is not None: + if (upper_stack := up_stack.f_back) is not None: + stack_trace = upper_stack + else: + stack_trace = up_stack + else: + stack_trace = stack + + message = LogMessage(messages=messages, + end=end, + split=split, + flush=flush, + level=level, + log_time=log_time, + logger_name=self.log_name, + logger_tag=tag, + stack_trace=stack_trace) + if level >= 30: # WARN + for output in self.outputs: + output.write_stderr(message) + else: + for output in self.outputs: + output.write_stdout(message) + # done? + # 20231106 00:06 + + @staticmethod + def get_logger_by_name(name: str) -> 'Logger': + """ + Get a logger by name. + + Args: + name (str): The name of the logger. + + Returns: + Logger: The logger with the specified name. + """ + return Logger(log_name=name) + + def info(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.info): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.info, + stack_trace=stack_trace) + + def trace(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.trace): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.trace, + stack_trace=stack_trace) + + def fine(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.fine): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.fine, + stack_trace=stack_trace) + + def debug(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.debug): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.debug, + stack_trace=stack_trace) + + def warn(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.warn): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.warn, + stack_trace=stack_trace) + + def error(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.error): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.error, + stack_trace=stack_trace) + + def fatal(self, + *message, + tag: Optional[str] = None, + end: str = '\n', + split: str = ' ', + flush: bool = True, + stack_trace: Optional[FrameType] = None) -> None: + if not self.log_for(LogLevel.fatal): + return + self.make_log(messages=list(message), + tag=tag, + end=end, + split=split, + flush=flush, + level=LogLevel.fatal, + stack_trace=stack_trace) diff --git a/libs/lib_not_dr/logger/outstream.py b/libs/lib_not_dr/logger/outstream.py new file mode 100644 index 0000000..df33209 --- /dev/null +++ b/libs/lib_not_dr/logger/outstream.py @@ -0,0 +1,236 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import io +import sys +import time +import string +import atexit +import threading + +from pathlib import Path +from typing import Optional + +from lib_not_dr.logger import LogLevel +from lib_not_dr.types.options import Options +from lib_not_dr.logger.structure import LogMessage +from lib_not_dr.logger.formatter import BaseFormatter, StdFormatter + +__all__ = [ + 'BaseOutputStream', + 'StdioOutputStream', + 'FileCacheOutputStream' +] + + +class BaseOutputStream(Options): + name = 'BaseOutputStream' + + level: int = LogLevel.info + enable: bool = True + + formatter: BaseFormatter + + def write_stdout(self, message: LogMessage) -> None: + raise NotImplementedError(f'{self.__class__.__name__}.write_stdout is not implemented') + + def write_stderr(self, message: LogMessage) -> None: + raise NotImplementedError(f'{self.__class__.__name__}.write_stderr is not implemented') + + def flush(self) -> None: + raise NotImplementedError(f'{self.__class__.__name__}.flush is not implemented') + + def close(self) -> None: + self.enable = False + + +class StdioOutputStream(BaseOutputStream): + name = 'StdioOutputStream' + + level: int = LogLevel.info + formatter: BaseFormatter = StdFormatter() + use_stderr: bool = True + + def write_stdout(self, message: LogMessage) -> None: + if not self.enable: + return None + if message.level < self.level: + return None + print(self.formatter.format_message(message), end='', flush=message.flush) + return None + + def write_stderr(self, message: LogMessage) -> None: + if not self.enable: + return None + if message.level < self.level: + return None + if self.use_stderr: + print(self.formatter.format_message(message), end='', flush=message.flush, file=sys.stderr) + else: + print(self.formatter.format_message(message), end='', flush=message.flush) + return None + + def flush(self) -> None: + """ + flush stdout and stderr + :return: None + """ + print('', end='', flush=True) + print('', end='', flush=True, file=sys.stderr) + return None + + +class FileCacheOutputStream(BaseOutputStream): + name = 'FileCacheOutputStream' + + level: int = LogLevel.info + formatter: BaseFormatter = StdFormatter(enable_color=False) + text_cache: io.StringIO = None + + flush_counter: int = 0 + # 默认 10 次 flush 一次 + flush_count_limit: int = 10 + flush_time_limit: int = 10 # time limit in sec, 0 means no limit + flush_timer: threading.Timer = None + + file_path: Optional[Path] = Path('./logs') + file_name: str + # file mode: always 'a' + file_encoding: str = 'utf-8' + # do file swap or not + file_swap: bool = False + at_exit_register: bool = False + + file_swap_counter: int = 0 + file_swap_name_template: str = '${name}-${counter}.log' + # ${name} -> file_name + # ${counter} -> file_swap_counter + # ${log_time} -> time when file swap ( round(time.time()) ) + # ${start_time} -> start time of output stream ( round(time.time()) ) + current_file_name: str = None + file_start_time: int = None + + # log file swap triggers + # 0 -> no limit + file_size_limit: int = 0 # size limit in kb + file_time_limit: int = 0 # time limit in sec 0 + file_swap_on_both: bool = False # swap file when both size and time limit reached + + def init(self, **kwargs) -> bool: + self.file_start_time = round(time.time()) + if self.text_cache is None: + self.text_cache = io.StringIO() + self.get_file_path() + return False + + def _write(self, message: LogMessage) -> None: + """ + write message to text cache + 默认已经检查过了 + :param message: message to write + :return: None + """ + self.text_cache.write(self.formatter.format_message(message)) + self.flush_counter += 1 + if message.flush or self.flush_counter >= self.flush_count_limit: + self.flush() + else: + if self.flush_time_limit > 0: + if self.flush_timer is None or not self.flush_timer.is_alive(): + self.flush_timer = threading.Timer(self.flush_time_limit, self.flush) + self.flush_timer.daemon = True + self.flush_timer.start() + if not self.at_exit_register: + atexit.register(self.flush) + self.at_exit_register = True + return None + + def write_stdout(self, message: LogMessage) -> None: + if not self.enable: + return None + if message.level < self.level: + return None + self._write(message) + return None + + def write_stderr(self, message: LogMessage) -> None: + if not self.enable: + return None + if message.level < self.level: + return None + self._write(message) + return None + + def get_file_path(self) -> Path: + """ + get file path + :return: + """ + if (current_file := self.current_file_name) is None: + if not self.file_swap: + # 直接根据 file name 生成文件 + current_file = Path(self.file_path) / self.file_name + self.current_file_name = str(current_file) + return current_file + template = string.Template(self.file_swap_name_template) + file_name = template.safe_substitute(name=self.file_name, + counter=self.file_swap_counter, + log_time=round(time.time()), + start_time=self.file_start_time) + current_file = Path(self.file_path) / file_name + self.current_file_name = str(current_file) + else: + current_file = Path(current_file) + return current_file + + def check_flush(self) -> Path: + current_file = self.get_file_path() + # 获取当前文件的路径 + if not self.file_swap: + # 不需要 swap 直接返回 + return current_file + # 检查是否需要 swap + size_pass = True + if self.file_size_limit > 0: + file_size = current_file.stat().st_size / 1024 # kb + if file_size > self.file_size_limit: # kb + size_pass = False + time_pass = True + if self.file_time_limit > 0: + file_time = round(time.time()) - current_file.stat().st_mtime + if file_time > self.file_time_limit: + time_pass = False + if (self.file_swap_on_both and size_pass and time_pass) or \ + (not self.file_swap_on_both and (size_pass or time_pass)): + # 两个都满足 + # 或者只有一个满足 + if size_pass and time_pass: + self.file_swap_counter += 1 + # 生成新的文件名 + return self.get_file_path() + + def flush(self) -> None: + new_cache = io.StringIO() # 创建新的缓存 + self.flush_counter = 0 # atomic, no lock + old_cache, self.text_cache = self.text_cache, new_cache + text = old_cache.getvalue() + old_cache.close() # 关闭旧的缓存 + if text == '': + return None + current_file = self.check_flush() + if not current_file.exists(): + current_file.parent.mkdir(parents=True, exist_ok=True) + current_file.touch(exist_ok=True) + with current_file.open('a', encoding=self.file_encoding) as f: + f.write(text) + return None + + def close(self) -> None: + super().close() + self.flush() + self.text_cache.close() + atexit.unregister(self.flush) + return None diff --git a/libs/utils/logger/structer.md b/libs/lib_not_dr/logger/structer.md similarity index 100% rename from libs/utils/logger/structer.md rename to libs/lib_not_dr/logger/structer.md diff --git a/libs/lib_not_dr/logger/structure.py b/libs/lib_not_dr/logger/structure.py new file mode 100644 index 0000000..b8ee936 --- /dev/null +++ b/libs/lib_not_dr/logger/structure.py @@ -0,0 +1,99 @@ +# ------------------------------- +# Difficult Rocket +# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com +# All rights reserved +# ------------------------------- + +import time + +from pathlib import Path +from types import FrameType +from typing import List, Optional, Tuple, Dict, Union + +from lib_not_dr.types.options import Options + +__all__ = ['LogMessage', + 'FormattingMessage'] + + +class LogMessage(Options): + name = 'LogMessage' + + # 消息内容本身的属性 + messages: List[str] = [] + end: str = '\n' + split: str = ' ' + + # 消息的属性 + flush: bool = None + level: int = 20 + log_time: float = None # time.time_ns() + logger_name: str = 'root' + logger_tag: Optional[str] = None + stack_trace: Optional[FrameType] = None + + def __init__(self, + messages: Optional[List[str]] = None, + end: Optional[str] = '\n', + split: Optional[str] = ' ', + flush: Optional[bool] = None, + level: Optional[int] = 20, + log_time: Optional[float] = None, + logger_name: Optional[str] = 'root', + logger_tag: Optional[str] = None, + stack_trace: Optional[FrameType] = None, + **kwargs) -> None: + """ + Init for LogMessage + :param messages: message list for log + :param end: end of message + :param split: split for messages + :param flush: do flush or not + :param level: level of message + :param log_time: time of message (default: time.time_ns()) + :param logger_name: name of logger + :param logger_tag: tag of logger + :param stack_trace: stack trace of logger + :param kwargs: other options + """ + # 为了方便使用 单独覆盖了 __init__ 方法来提供代码补全的选项 + super().__init__(messages=messages, + end=end, + split=split, + flush=flush, + level=level, + log_time=log_time, + logger_name=logger_name, + logger_tag=logger_tag, + stack_trace=stack_trace, + **kwargs) + + def init(self, **kwargs) -> bool: + if self.log_time is None: + self.log_time = time.time_ns() + if not isinstance(self.flush, bool) and self.flush is not None: + self.flush = True if self.flush else False + return False + + def format_message(self) -> str: + return self.split.join(self.messages) + self.end + + def format_for_message(self) -> Dict[str, str]: + basic_info = self.option() + + if self.logger_tag is None: + basic_info['logger_tag'] = ' ' + + basic_info['messages'] = self.format_message() + + return basic_info + + @property + def create_msec_3(self) -> int: + return int(self.log_time / 1000000) % 1000 + + +FormattingMessage = Tuple[LogMessage, Dict[str, Union[str, Path]]] + +if __name__ == '__main__': + print(LogMessage().as_markdown()) diff --git a/libs/lib_not_dr/types/options.py b/libs/lib_not_dr/types/options.py index 5ae313e..6f58357 100644 --- a/libs/lib_not_dr/types/options.py +++ b/libs/lib_not_dr/types/options.py @@ -64,6 +64,7 @@ class Options: """ name = 'Option Base' cached_options: Dict[str, Union[str, Any]] = {} + _check_options: bool = True def __init__(self, **kwargs): """ @@ -75,7 +76,7 @@ class Options: self._options: Dict[str, Union[Callable, object]] = {} self.flush_option() for option, value in kwargs.items(): - if option not in self.cached_options: + if option not in self.cached_options and self._check_options: raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined") setattr(self, option, value) run_load_file = True diff --git a/libs/pyglet/customtypes.py b/libs/pyglet/customtypes.py index d792cc5..0746f71 100644 --- a/libs/pyglet/customtypes.py +++ b/libs/pyglet/customtypes.py @@ -1,12 +1,16 @@ """Holds type aliases used throughout the codebase.""" import ctypes +import sys from typing import Union - __all__ = [ - "Buffer" + "Buffer", ] -# Backwards compatible placeholder for `collections.abc.Buffer` from Python 3.12 -Buffer = Union[bytes, bytearray, memoryview, ctypes.Array] + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + # Best-effort placeholder for older Python versions + Buffer = Union[bytes, bytearray, memoryview, ctypes.Array] diff --git a/libs/pyglet/experimental/README.md b/libs/pyglet/experimental/README.md new file mode 100644 index 0000000..ca3759f --- /dev/null +++ b/libs/pyglet/experimental/README.md @@ -0,0 +1,6 @@ +Experimental modules +==================== + +This package contains experimental modules, which are included here for +wider testing and feedback. Anything contined within may be broken, refactored, +or removed without notice. \ No newline at end of file diff --git a/libs/pyglet/experimental/__init__.py b/libs/pyglet/experimental/__init__.py deleted file mode 100644 index 5d1448b..0000000 --- a/libs/pyglet/experimental/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Experimental modules. - -This package contains experimental modules, included here for wider testing -and experimentation. Anything contined within may be broken, refactored, or -removed without notice. -""" \ No newline at end of file diff --git a/libs/pyglet/experimental/geoshader_sprite.py b/libs/pyglet/experimental/geoshader_sprite.py index 9ffd3f8..92b31b5 100644 --- a/libs/pyglet/experimental/geoshader_sprite.py +++ b/libs/pyglet/experimental/geoshader_sprite.py @@ -148,27 +148,15 @@ fragment_array_source = """#version 150 core def get_default_shader(): - try: - return pyglet.gl.current_context.pyglet_sprite_default_shader - except AttributeError: - vert_shader = graphics.shader.Shader(vertex_source, 'vertex') - geom_shader = graphics.shader.Shader(geometry_source, 'geometry') - frag_shader = graphics.shader.Shader(fragment_source, 'fragment') - default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader) - pyglet.gl.current_context.pyglet_sprite_default_shader = default_shader_program - return pyglet.gl.current_context.pyglet_sprite_default_shader + return pyglet.gl.current_context.create_program((vertex_source, 'vertex'), + (geometry_source, 'geometry'), + (fragment_source, 'fragment')) def get_default_array_shader(): - try: - return pyglet.gl.current_context.pyglet_sprite_default_array_shader - except AttributeError: - vert_shader = graphics.shader.Shader(vertex_source, 'vertex') - geom_shader = graphics.shader.Shader(geometry_source, 'geometry') - frag_shader = graphics.shader.Shader(fragment_array_source, 'fragment') - default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader) - pyglet.gl.current_context.pyglet_sprite_default_array_shader = default_shader_program - return pyglet.gl.current_context.pyglet_sprite_default_array_shader + return pyglet.gl.current_context.create_program((vertex_source, 'vertex'), + (geometry_source, 'geometry'), + (fragment_array_source, 'fragment')) class SpriteGroup(graphics.Group): diff --git a/libs/pyglet/experimental/net.py b/libs/pyglet/experimental/net.py new file mode 100644 index 0000000..d5d584c --- /dev/null +++ b/libs/pyglet/experimental/net.py @@ -0,0 +1,247 @@ +"""Experimental networking + +This module contains experiments in making user-friendly Server and Client +classes that integrate with pyglet's event system. These are very basic, +socket server/client examples, and are not ready to be used in production. +They are included here to solicit feedback, and possibly spark further +development. Basic Server usage:: + + server = net.Server(address='0.0.0.0', port=1234) + active_connections = weakref.WeakSet() + + def pong(connection, message): + print(f"Received '{message}' from '{connection}'") + connection.send(b'pong') + + @server.event + def on_connection(connection): + print(f"New client connected: {connection}") + connection.set_handler('on_receive', pong) + active_connections.add(connection) + + @server.event + def on_disconnect(connection): + print(f"Client disconnected: {connection}") + active_connections.discard(connection) + + +Basic Client example:: + + client = net.Client(address='localhost', port=1234) + + @client.event + def on_receive(client, message): + print(f"Received: {message}") + + @client.event + def on_disconnect(client): + print(f"Disconnected: {client}") + + client.send(b'ping') + +""" + + +import queue as _queue +import struct as _struct +import socket as _socket +import asyncio as _asyncio +import threading as _threading + +from pyglet.event import EventDispatcher as _EventDispatcher + +from pyglet.util import debug_print + +_debug_net = debug_print('debug_net') + + +class Client(_EventDispatcher): + def __init__(self, address, port): + """Create a Client connection to a Server.""" + self._socket = _socket.create_connection((address, port)) + self._address = address + self._port = port + + self._terminate = _threading.Event() + self._queue = _queue.Queue() + + _threading.Thread(target=self._recv, daemon=True).start() + _threading.Thread(target=self._send, daemon=True).start() + + self._sentinal = object() # poison pill + + def close(self): + """Close the connection.""" + self._queue.put(self._sentinal) + self._socket.shutdown(1) + if not self._terminate.is_set(): + self._terminate.set() + self.dispatch_event('on_disconnect', self) + + def send(self, message): + """Queue a message to send. + + Put a string of bytes into the queue to send. + raises a `ConnectionError` if the connection + has been closed or dropped. + + :Parameters: + `message` : bytes + A string of bytes to send. + """ + if self._terminate.is_set(): + raise ConnectionError("Connection is closed.") + self._queue.put(message) + + def _send(self): # Thread + """Background Thread to send messages from the queue.""" + while not self._terminate.is_set(): + message = self._queue.get() + if message == self._sentinal: # bail out on poison pill + break + try: + # Attach a 4byte header to the front of the message: + packet = _struct.pack('I', len(message)) + message + self._socket.sendall(packet) + except (ConnectionError, OSError): + self.close() + break + + assert _debug_net("Exiting _send thread") + + + def _recv(self): # Thread + socket = self._socket + + while not self._terminate.is_set(): + try: + header = socket.recv(4) + while len(header) < 4: + header += socket.recv(4 - len(header)) + size = _struct.unpack('I', header)[0] + + message = socket.recv(size) + while len(message) < size: + message += socket.recv(size) + self.dispatch_event('on_receive', self, message) + except (ConnectionError, OSError): + self.close() + break + + assert _debug_net("Exiting _recv thread") + + def on_receive(self, connection, message): + """Event for received messages.""" + + def on_disconnect(self, connection): + """Event for disconnection. """ + + def __repr__(self): + return f"Client(address={self._address}, port={self._port})" + + +Client.register_event_type('on_receive') +Client.register_event_type('on_disconnect') + + +class ClientConnection(_EventDispatcher): + + def __init__(self, reader, writer): + self._reader = reader + self._writer = writer + self._closed = False + self._loop = _asyncio.get_event_loop() + _asyncio.run_coroutine_threadsafe(self._recv(), self._loop) + + def close(self): + if not self._closed: + self._writer.transport.close() + self._closed = True + self.dispatch_event('on_disconnect', self) + + async def _recv(self): + while not self._closed: + try: + header = await self._reader.readexactly(4) + size = _struct.unpack('I', header)[0] + message = await self._reader.readexactly(size) + self._loop.call_soon(self.dispatch_event, 'on_receive', self, message) + + except _asyncio.IncompleteReadError: + self.close() + break + + async def _send(self, message): + try: + packet = _struct.pack('I', len(message)) + message + self._writer.write(packet) + await self._writer.drain() + except ConnectionResetError: + self.close() + + def send(self, message): + # Synchrounously send a message in an async coroutine. + if self._writer.transport is None or self._writer.transport.is_closing(): + self.close() + return + _future = _asyncio.run_coroutine_threadsafe(self._send(message), self._loop) + + def on_receive(self, connection, message): + """Event for received messages.""" + + def on_disconnect(self, connection): + """Event for disconnection. """ + + def __del__(self): + assert _debug_net(f"Connection garbage collected: {self}") + + def __repr__(self): + return f"{self.__class__.__name__}({id(self)})" + + +ClientConnection.register_event_type('on_receive') +ClientConnection.register_event_type('on_disconnect') + + +class Server(_EventDispatcher): + + def __init__(self, address, port): + self._address = address + self._port = port + + self._server = None + + self._thread = _threading.Thread(target=self._run, daemon=True) + self._thread.start() + + blurb = f"Server listening on {address}:{port}" + assert _debug_net(f"{'-' * len(blurb)}\n{blurb}\n{'-' * len(blurb)}") + + + async def handle_connection(self, reader, writer): + connection = ClientConnection(reader, writer) + self.dispatch_event('on_connection', connection) + + async def _start_server(self): + self._server = await _asyncio.start_server(self.handle_connection, self._address, self._port) + async with self._server: + await self._server.serve_forever() + + def _run(self): + try: + _asyncio.run(self._start_server()) + except KeyboardInterrupt: + self._server.close() + + def on_connection(self, connection): + """Event for new Client connections.""" + assert _debug_net(f"Connected <--- {connection}") + connection.set_handler('on_disconnect', self.on_disconnect) + + def on_disconnect(self, connection): + """Event for disconnected Clients.""" + assert _debug_net(f"Disconnected ---> {connection}") + + +Server.register_event_type('on_connection') +Server.register_event_type('on_disconnect') diff --git a/libs/pyglet/font/directwrite.py b/libs/pyglet/font/directwrite.py index fcf1997..232c1fa 100644 --- a/libs/pyglet/font/directwrite.py +++ b/libs/pyglet/font/directwrite.py @@ -266,30 +266,29 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure): class IDWriteFontFileStream(com.IUnknown): _methods_ = [ ('ReadFileFragment', - com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), + com.STDMETHOD(POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))), ('ReleaseFileFragment', - com.STDMETHOD(c_void_p, c_void_p)), + com.STDMETHOD(c_void_p)), ('GetFileSize', - com.STDMETHOD(c_void_p, POINTER(UINT64))), + com.STDMETHOD(POINTER(UINT64))), ('GetLastWriteTime', - com.STDMETHOD(c_void_p, POINTER(UINT64))), + com.STDMETHOD(POINTER(UINT64))), ] class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only. _methods_ = [ ('CreateStreamFromKey', - com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) ] class IDWriteFontFileLoader(com.pIUnknown): _methods_ = [ ('CreateStreamFromKey', - com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) + com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream)))) ] - class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown): _methods_ = [ ('GetFilePathLengthFromKey', @@ -452,13 +451,13 @@ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0 class IDWriteTextAnalysisSource(com.IUnknown): _methods_ = [ ('GetTextAtPosition', - com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))), + com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))), ('GetTextBeforePosition', - com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))), + com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))), ('GetParagraphReadingDirection', com.METHOD(DWRITE_READING_DIRECTION)), ('GetLocaleName', - com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))), + com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))), ('GetNumberSubstitution', com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)), ] @@ -467,7 +466,7 @@ class IDWriteTextAnalysisSource(com.IUnknown): class IDWriteTextAnalysisSink(com.IUnknown): _methods_ = [ ('SetScriptAnalysis', - com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))), + com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))), ('SetLineBreakpoints', com.STDMETHOD(UINT32, UINT32, c_void_p)), ('SetBidiLevel', @@ -524,7 +523,7 @@ class TextAnalysis(com.COMObject): analyzer.AnalyzeScript(self, 0, text_length, self) - def SetScriptAnalysis(self, this, textPosition, textLength, scriptAnalysis): + def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis): # textPosition - The index of the first character in the string that the result applies to # textLength - How many characters of the string from the index that the result applies to # scriptAnalysis - The analysis information for all glyphs starting at position for length. @@ -542,10 +541,10 @@ class TextAnalysis(com.COMObject): return 0 # return 0x80004001 - def GetTextBeforePosition(self, this, textPosition, textString, textLength): + def GetTextBeforePosition(self, textPosition, textString, textLength): raise Exception("Currently not implemented.") - def GetTextAtPosition(self, this, textPosition, textString, textLength): + def GetTextAtPosition(self, textPosition, textString, textLength): # This method will retrieve a substring of the text in this layout # to be used in an analysis step. # Arguments: @@ -568,7 +567,7 @@ class TextAnalysis(com.COMObject): def GetParagraphReadingDirection(self): return 0 - def GetLocaleName(self, this, textPosition, textLength, localeName): + def GetLocaleName(self, textPosition, textLength, localeName): self.__local_name = c_wchar_p("") # TODO: Add more locales. localeName[0] = self.__local_name textLength[0] = self._textlength - textPosition @@ -954,16 +953,16 @@ class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown): class IDWriteFontFileEnumerator(com.IUnknown): _methods_ = [ ('MoveNext', - com.STDMETHOD(c_void_p, POINTER(BOOL))), + com.STDMETHOD(POINTER(BOOL))), ('GetCurrentFontFile', - com.STDMETHOD(c_void_p, c_void_p)), + com.STDMETHOD(c_void_p)), ] class IDWriteFontCollectionLoader(com.IUnknown): _methods_ = [ ('CreateEnumeratorFromKey', - com.STDMETHOD(c_void_p, c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))), + com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))), ] @@ -971,20 +970,12 @@ class MyFontFileStream(com.COMObject): _interfaces_ = [IDWriteFontFileStream] def __init__(self, data): + super().__init__() self._data = data self._size = len(data) self._ptrs = [] - def AddRef(self, this): - return 1 - - def Release(self, this): - return 1 - - def QueryInterface(self, this, refiid, tester): - return 0 - - def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext): + def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext): if fileOffset + fragmentSize > self._size: return 0x80004005 # E_FAIL @@ -997,14 +988,14 @@ class MyFontFileStream(com.COMObject): fragmentContext[0] = None return 0 - def ReleaseFileFragment(self, this, fragmentContext): + def ReleaseFileFragment(self, fragmentContext): return 0 - def GetFileSize(self, this, fileSize): + def GetFileSize(self, fileSize): fileSize[0] = self._size return 0 - def GetLastWriteTime(self, this, lastWriteTime): + def GetLastWriteTime(self, lastWriteTime): return 0x80004001 # E_NOTIMPL @@ -1012,21 +1003,13 @@ class LegacyFontFileLoader(com.COMObject): _interfaces_ = [IDWriteFontFileLoader_LI] def __init__(self): + super().__init__() self._streams = {} - def QueryInterface(self, this, refiid, tester): - return 0 - - def AddRef(self, this): - return 1 - - def Release(self, this): - return 1 - - def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream): + def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream): convert_index = cast(fontfileReferenceKey, POINTER(c_uint32)) - self._ptr = ctypes.cast(self._streams[convert_index.contents.value]._pointers[IDWriteFontFileStream], + self._ptr = ctypes.cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream), POINTER(IDWriteFontFileStream)) fontFileStream[0] = self._ptr return 0 @@ -1039,6 +1022,7 @@ class MyEnumerator(com.COMObject): _interfaces_ = [IDWriteFontFileEnumerator] def __init__(self, factory, loader): + super().__init__() self.factory = cast(factory, IDWriteFactory) self.key = "pyglet_dwrite" self.size = len(self.key) @@ -1057,7 +1041,7 @@ class MyEnumerator(com.COMObject): def AddFontData(self, fonts): self._font_data = fonts - def MoveNext(self, this, hasCurrentFile): + def MoveNext(self, hasCurrentFile): self.current_index += 1 if self.current_index != len(self._font_data): @@ -1087,7 +1071,7 @@ class MyEnumerator(com.COMObject): pass - def GetCurrentFontFile(self, this, fontFile): + def GetCurrentFontFile(self, fontFile): fontFile = cast(fontFile, POINTER(IDWriteFontFile)) fontFile[0] = self._font_files[self.current_index] return 0 @@ -1097,24 +1081,14 @@ class LegacyCollectionLoader(com.COMObject): _interfaces_ = [IDWriteFontCollectionLoader] def __init__(self, factory, loader): + super().__init__() self._enumerator = MyEnumerator(factory, loader) def AddFontData(self, fonts): self._enumerator.AddFontData(fonts) - def AddRef(self, this): - self._i = 1 - return 1 - - def Release(self, this): - self._i = 0 - return 1 - - def QueryInterface(self, this, refiid, tester): - return 0 - - def CreateEnumeratorFromKey(self, this, factory, key, key_size, enumerator): - self._ptr = ctypes.cast(self._enumerator._pointers[IDWriteFontFileEnumerator], + def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator): + self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator), POINTER(IDWriteFontFileEnumerator)) enumerator[0] = self._ptr @@ -2418,7 +2392,7 @@ class Win32DirectWriteFont(base.Font): # Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface. # Therefore we need to pass to the actual pointer directly. - cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) + cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI)) cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) @@ -2472,7 +2446,7 @@ class Win32DirectWriteFont(base.Font): cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader) cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader) - cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI]) + cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI)) cls._font_collection_loader.AddFontData(cls._font_cache) diff --git a/libs/pyglet/font/win32.py b/libs/pyglet/font/win32.py index 67ea3c5..108ff1a 100644 --- a/libs/pyglet/font/win32.py +++ b/libs/pyglet/font/win32.py @@ -336,7 +336,7 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer): pass def _create_bitmap(self, width, height): - self._data = (ctypes.c_byte * (4 * width * height))() + self._data = (BYTE * (4 * width * height))() self._bitmap = ctypes.c_void_p() self._format = PixelFormat32bppARGB gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4, @@ -532,6 +532,12 @@ class GDIPlusFont(Win32Font): self._name = name family = ctypes.c_void_p() + + # GDI will add @ in front of a localized font for some Asian languages. However, GDI will also not find it + # based on that name (???). Here we remove it before checking font collections. + if name[0] == "@": + name = name[1:] + name = ctypes.c_wchar_p(name) # Look in private collection first: @@ -540,6 +546,9 @@ class GDIPlusFont(Win32Font): # Then in system collection: if not family: + if _debug_font: + print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}") + gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family)) # Nothing found, use default font. diff --git a/libs/pyglet/gl/base.py b/libs/pyglet/gl/base.py index ecec0d6..9398eea 100644 --- a/libs/pyglet/gl/base.py +++ b/libs/pyglet/gl/base.py @@ -1,6 +1,7 @@ import weakref from enum import Enum +import threading from typing import Tuple import pyglet @@ -209,11 +210,12 @@ class CanvasConfig(Config): class ObjectSpace: def __init__(self): - # Textures and buffers scheduled for deletion - # the next time this object space is active. + # Objects scheduled for deletion the next time this object space is active. self.doomed_textures = [] self.doomed_buffers = [] self.doomed_shader_programs = [] + self.doomed_shaders = [] + self.doomed_renderbuffers = [] class Context: @@ -236,6 +238,7 @@ class Context: self.canvas = None self.doomed_vaos = [] + self.doomed_framebuffers = [] if context_share: self.object_space = context_share.object_space @@ -277,28 +280,49 @@ class Context: self._info = gl_info.GLInfo() self._info.set_active_context() - # Release Textures, Buffers, and VAOs on this context scheduled for - # deletion. Note that the garbage collector may introduce a race - # condition, so operate on a copy, and clear the list afterward. if self.object_space.doomed_textures: - textures = self.object_space.doomed_textures[:] - textures = (gl.GLuint * len(textures))(*textures) - gl.glDeleteTextures(len(textures), textures) - self.object_space.doomed_textures.clear() + self._delete_objects(self.object_space.doomed_textures, gl.glDeleteTextures) if self.object_space.doomed_buffers: - buffers = self.object_space.doomed_buffers[:] - buffers = (gl.GLuint * len(buffers))(*buffers) - gl.glDeleteBuffers(len(buffers), buffers) - self.object_space.doomed_buffers.clear() + self._delete_objects(self.object_space.doomed_buffers, gl.glDeleteBuffers) if self.object_space.doomed_shader_programs: - for program_id in self.object_space.doomed_shader_programs: - gl.glDeleteProgram(program_id) - self.object_space.doomed_shader_programs.clear() + self._delete_objects_one_by_one(self.object_space.doomed_shader_programs, + gl.glDeleteProgram) + if self.object_space.doomed_shaders: + self._delete_objects_one_by_one(self.object_space.doomed_shaders, gl.glDeleteShader) + if self.object_space.doomed_renderbuffers: + self._delete_objects(self.object_space.doomed_renderbuffers, gl.glDeleteRenderbuffers) + if self.doomed_vaos: - vaos = self.doomed_vaos[:] - vaos = (gl.GLuint * len(vaos))(*vaos) - gl.glDeleteVertexArrays(len(vaos), vaos) - self.doomed_vaos.clear() + self._delete_objects(self.doomed_vaos, gl.glDeleteVertexArrays) + if self.doomed_framebuffers: + self._delete_objects(self.doomed_framebuffers, gl.glDeleteFramebuffers) + + # For the functions below: + # The garbage collector introduces a race condition. + # The provided list might be appended to (and only appended to) while this + # method runs, as it's a `doomed_*` list either on the context or its + # object space. If `count` wasn't stored in a local, this method might + # leak objects. + def _delete_objects(self, list_, deletion_func): + """Release all OpenGL objects in the given list using the supplied + deletion function with the signature ``(GLuint count, GLuint *names)``. + """ + count = len(list_) + to_delete = list_[:count] + del list_[:count] + + deletion_func(count, (gl.GLuint * count)(*to_delete)) + + def _delete_objects_one_by_one(self, list_, deletion_func): + """Similar to ``_delete_objects``, but assumes the deletion functions's + signature to be ``(GLuint name)``, calling it once for each object. + """ + count = len(list_) + to_delete = list_[:count] + del list_[:count] + + for name in to_delete: + deletion_func(gl.GLuint(name)) def destroy(self): """Release the context. @@ -318,6 +342,27 @@ class Context: if gl._shadow_window is not None: gl._shadow_window.switch_to() + def _safe_to_operate_on_object_space(self): + """Return whether it is safe to interact with this context's object + space. + + This is considered to be the case if the currently active context's + object space is the same as this context's object space and this + method is called from the main thread. + """ + return ( + self.object_space is gl.current_context.object_space and + threading.current_thread() is threading.main_thread() + ) + + def _safe_to_operate_on(self): + """Return whether it is safe to interact with this context. + + This is considered to be the case if it's the current context and this + method is called from the main thread. + """ + return gl.current_context is self and threading.current_thread() is threading.main_thread() + def create_program(self, *sources: Tuple[str, str], program_class=None): """Create a ShaderProgram from OpenGL GLSL source. @@ -347,25 +392,30 @@ class Context: return program def delete_texture(self, texture_id): - """Safely delete a Texture belonging to this context. + """Safely delete a Texture belonging to this context's object space. - Usually, the Texture is released immediately using - ``glDeleteTextures``, however if another context that does not share - this context's object space is currently active, the deletion will - be deferred until an appropriate context is activated. + This method will delete the texture immediately via + ``glDeleteTextures`` if the current context's object space is the same + as this context's object space and it is called from the main thread. + + Otherwise, the texture will only be marked for deletion, postponing + it until any context with the same object space becomes active again. + + This makes it safe to call from anywhere, including other threads. :Parameters: `texture_id` : int The OpenGL name of the Texture to delete. """ - if self.object_space is gl.current_context.object_space: + if self._safe_to_operate_on_object_space(): gl.glDeleteTextures(1, gl.GLuint(texture_id)) else: self.object_space.doomed_textures.append(texture_id) def delete_buffer(self, buffer_id): - """Safely delete a Buffer object belonging to this context. + """Safely delete a Buffer object belonging to this context's object + space. This method behaves similarly to `delete_texture`, though for ``glDeleteBuffers`` instead of ``glDeleteTextures``. @@ -376,30 +426,14 @@ class Context: .. versionadded:: 1.1 """ - if self.object_space is gl.current_context.object_space and False: + if self._safe_to_operate_on_object_space(): gl.glDeleteBuffers(1, gl.GLuint(buffer_id)) else: self.object_space.doomed_buffers.append(buffer_id) - def delete_vao(self, vao_id): - """Safely delete a Vertex Array Object belonging to this context. - - This method behaves similarly to `delete_texture`, though for - ``glDeleteVertexArrays`` instead of ``glDeleteTextures``. - - :Parameters: - `vao_id` : int - The OpenGL name of the Vertex Array to delete. - - .. versionadded:: 2.0 - """ - if gl.current_context is self: - gl.glDeleteVertexArrays(1, gl.GLuint(vao_id)) - else: - self.doomed_vaos.append(vao_id) - def delete_shader_program(self, program_id): - """Safely delete a Shader Program belonging to this context. + """Safely delete a Shader Program belonging to this context's + object space. This method behaves similarly to `delete_texture`, though for ``glDeleteProgram`` instead of ``glDeleteTextures``. @@ -410,11 +444,84 @@ class Context: .. versionadded:: 2.0 """ - if gl.current_context is self: - gl.glDeleteProgram(program_id) + if self._safe_to_operate_on_object_space(): + gl.glDeleteProgram(gl.GLuint(program_id)) else: self.object_space.doomed_shader_programs.append(program_id) + def delete_shader(self, shader_id): + """Safely delete a Shader belonging to this context's object space. + + This method behaves similarly to `delete_texture`, though for + ``glDeleteShader`` instead of ``glDeleteTextures``. + + :Parameters: + `shader_id` : int + The OpenGL name of the Shader to delete. + + .. versionadded:: 2.0.10 + """ + if self._safe_to_operate_on_object_space(): + gl.glDeleteShader(gl.GLuint(shader_id)) + else: + self.object_space.doomed_shaders.append(shader_id) + + def delete_renderbuffer(self, rbo_id): + """Safely delete a Renderbuffer Object belonging to this context's + object space. + + This method behaves similarly to `delete_texture`, though for + ``glDeleteRenderbuffers`` instead of ``glDeleteTextures``. + + :Parameters: + `rbo_id` : int + The OpenGL name of the Shader Program to delete. + + .. versionadded:: 2.0.10 + """ + if self._safe_to_operate_on_object_space(): + gl.glDeleteRenderbuffers(1, gl.GLuint(rbo_id)) + else: + self.object_space.doomed_renderbuffers.append(rbo_id) + + def delete_vao(self, vao_id): + """Safely delete a Vertex Array Object belonging to this context. + + If this context is not the current context or this method is not + called from the main thread, its deletion will be postponed until + this context is next made active again. + + Otherwise, this method will immediately delete the VAO via + ``glDeleteVertexArrays``. + + :Parameters: + `vao_id` : int + The OpenGL name of the Vertex Array to delete. + + .. versionadded:: 2.0 + """ + if self._safe_to_operate_on(): + gl.glDeleteVertexArrays(1, gl.GLuint(vao_id)) + else: + self.doomed_vaos.append(vao_id) + + def delete_framebuffer(self, fbo_id): + """Safely delete a Framebuffer Object belonging to this context. + + This method behaves similarly to `delete_vao`, though for + ``glDeleteFramebuffers`` instead of ``glDeleteVertexArrays``. + + :Parameters: + `fbo_id` : int + The OpenGL name of the Framebuffer Object to delete. + + .. versionadded:: 2.0.10 + """ + if self._safe_to_operate_on(): + gl.glDeleteFramebuffers(1, gl.GLuint(fbo_id)) + else: + self.doomed_framebuffers.append(fbo_id) + def get_info(self): """Get the OpenGL information for this context. diff --git a/libs/pyglet/gl/lib.py b/libs/pyglet/gl/lib.py index d868430..9704e25 100644 --- a/libs/pyglet/gl/lib.py +++ b/libs/pyglet/gl/lib.py @@ -62,8 +62,7 @@ def errcheck(result, func, arguments): print(name) from pyglet import gl - context = gl.current_context - if not context: + if not gl.current_context: raise GLException('No GL context; create a Window first') error = gl.glGetError() if error: diff --git a/libs/pyglet/gl/win32.py b/libs/pyglet/gl/win32.py index 6cef71a..6e5b67f 100644 --- a/libs/pyglet/gl/win32.py +++ b/libs/pyglet/gl/win32.py @@ -244,7 +244,7 @@ class Win32ARBContext(_BaseWin32Context): if self.config.forward_compatible: flags |= wglext_arb.WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB if self.config.debug: - flags |= wglext_arb.WGL_DEBUG_BIT_ARB + flags |= wglext_arb.WGL_CONTEXT_DEBUG_BIT_ARB if flags: attribs.extend([wglext_arb.WGL_CONTEXT_FLAGS_ARB, flags]) attribs.append(0) diff --git a/libs/pyglet/graphics/__init__.py b/libs/pyglet/graphics/__init__.py index 4040c9a..9f6415b 100644 --- a/libs/pyglet/graphics/__init__.py +++ b/libs/pyglet/graphics/__init__.py @@ -55,10 +55,11 @@ def draw(size, mode, **data): assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt buffer = BufferObject(size * attribute.stride) - attribute.set_region(buffer, 0, size, array) + data = (attribute.c_type * len(array))(*array) + buffer.set_data(data) + attribute.enable() attribute.set_pointer(buffer.ptr) - buffers.append(buffer) # Don't garbage collect it. glDrawArrays(mode, 0, size) @@ -108,10 +109,12 @@ def draw_indexed(size, mode, indices, **data): assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt buffer = BufferObject(size * attribute.stride) - attribute.set_region(buffer, 0, size, array) + data = (attribute.c_type * len(array))(*array) + buffer.set_data(data) + attribute.enable() attribute.set_pointer(buffer.ptr) - buffers.append(buffer) + buffers.append(buffer) # Don't garbage collect it. if size <= 0xff: index_type = GL_UNSIGNED_BYTE diff --git a/libs/pyglet/graphics/shader.py b/libs/pyglet/graphics/shader.py index 61e8e1c..cb69d04 100644 --- a/libs/pyglet/graphics/shader.py +++ b/libs/pyglet/graphics/shader.py @@ -170,14 +170,14 @@ class Attribute: self.name = name self.location = location self.count = count - self.gl_type = gl_type - self.c_type = _c_types[gl_type] self.normalize = normalize - self.align = sizeof(self.c_type) - self.size = count * self.align - self.stride = self.size + self.c_type = _c_types[gl_type] + + self.element_size = sizeof(self.c_type) + self.byte_size = count * self.element_size + self.stride = self.byte_size def enable(self): """Enable the attribute.""" @@ -207,20 +207,16 @@ class Attribute: will be ``3 * 4 = 12``. :Parameters: - `buffer` : `AbstractMappable` + `buffer` : `AttributeBufferObject` The buffer to map. `start` : int Offset of the first vertex to map. `count` : int Number of vertices to map - :rtype: `AbstractBufferRegion` + :rtype: `BufferObjectRegion` """ - byte_start = self.stride * start - byte_size = self.stride * count - array_count = self.count * count - ptr_type = POINTER(self.c_type * array_count) - return buffer.get_region(byte_start, byte_size, ptr_type) + return buffer.get_region(start, count) def set_region(self, buffer, start, count, data): """Set the data over a region of the buffer. @@ -234,11 +230,7 @@ class Attribute: Number of vertices to set. `data` : A sequence of data components. """ - byte_start = self.stride * start - byte_size = self.stride * count - array_count = self.count * count - data = (self.c_type * array_count)(*data) - buffer.set_data_region(data, byte_start, byte_size) + buffer.set_region(start, count, data) def __repr__(self): return f"Attribute(name='{self.name}', location={self.location}, count={self.count})" @@ -682,6 +674,7 @@ class Shader: """ def __init__(self, source_string: str, shader_type: str): + self._context = pyglet.gl.current_context self._id = None self.type = shader_type @@ -697,6 +690,7 @@ class Shader: source_length = c_int(len(shader_source_utf8)) shader_id = glCreateShader(shader_type) + self._id = shader_id glShaderSource(shader_id, 1, byref(source_buffer_pointer), source_length) glCompileShader(shader_id) @@ -717,8 +711,6 @@ class Shader: elif _debug_gl_shaders: print(self._get_shader_log(shader_id)) - self._id = shader_id - @property def id(self): return self._id @@ -743,16 +735,19 @@ class Shader: glGetShaderSource(shader_id, source_length, None, source_str) return source_str.value.decode('utf8') - def __del__(self): - try: - glDeleteShader(self._id) - if _debug_gl_shaders: - print(f"Destroyed {self.type} Shader '{self._id}'") + def delete(self): + glDeleteShader(self._id) + self._id = None - except Exception: - # Interpreter is shutting down, - # or Shader failed to compile. - pass + def __del__(self): + if self._id is not None: + try: + self._context.delete_shader(self._id) + if _debug_gl_shaders: + print(f"Destroyed {self.type} Shader '{self._id}'") + self._id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down def __repr__(self): return "{0}(id={1}, type={2})".format(self.__class__.__name__, self.id, self.type) @@ -764,6 +759,8 @@ class ShaderProgram: __slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__' def __init__(self, *shaders: Shader): + self._id = None + assert shaders, "At least one Shader object is required." self._id = _link_program(*shaders) self._context = pyglet.gl.current_context @@ -807,13 +804,17 @@ class ShaderProgram: def __exit__(self, *_): glUseProgram(0) + def delete(self): + glDeleteProgram(self._id) + self._id = None + def __del__(self): - try: - self._context.delete_shader_program(self.id) - except Exception: - # Interpreter is shutting down, - # or ShaderProgram failed to link. - pass + if self._id is not None: + try: + self._context.delete_shader_program(self._id) + self._id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down def __setitem__(self, key, value): try: @@ -938,6 +939,8 @@ class ComputeShaderProgram: def __init__(self, source: str): """Create an OpenGL ComputeShaderProgram from source.""" + self._id = None + if not (gl_info.have_version(4, 3) or gl_info.have_extension("GL_ARB_compute_shader")): raise ShaderException("Compute Shader not supported. OpenGL Context version must be at least " "4.3 or higher, or 4.2 with the 'GL_ARB_compute_shader' extension.") @@ -1010,13 +1013,17 @@ class ComputeShaderProgram: def __exit__(self, *_): glUseProgram(0) + def delete(self): + glDeleteProgram(self._id) + self._id = None + def __del__(self): - try: - self._context.delete_shader_program(self.id) - except Exception: - # Interpreter is shutting down, - # or ShaderProgram failed to link. - pass + if self._id is not None: + try: + self._context.delete_shader_program(self._id) + self._id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down def __setitem__(self, key, value): try: diff --git a/libs/pyglet/graphics/vertexarray.py b/libs/pyglet/graphics/vertexarray.py index e4c2580..4071a50 100644 --- a/libs/pyglet/graphics/vertexarray.py +++ b/libs/pyglet/graphics/vertexarray.py @@ -27,10 +27,8 @@ class VertexArray: glBindVertexArray(0) def delete(self): - try: - glDeleteVertexArrays(1, self._id) - except Exception: - pass + glDeleteVertexArrays(1, self._id) + self._id = None __enter__ = bind @@ -38,11 +36,12 @@ class VertexArray: glBindVertexArray(0) def __del__(self): - try: - self._context.delete_vao(self.id) - # Python interpreter is shutting down: - except ImportError: - pass + if self._id is not None: + try: + self._context.delete_vao(self.id) + self._id = None + except (ImportError, AttributeError): + pass # Interpreter is shutting down def __repr__(self): return "{}(id={})".format(self.__class__.__name__, self._id.value) diff --git a/libs/pyglet/graphics/vertexbuffer.py b/libs/pyglet/graphics/vertexbuffer.py index ffbaa7f..cd3f864 100644 --- a/libs/pyglet/graphics/vertexbuffer.py +++ b/libs/pyglet/graphics/vertexbuffer.py @@ -11,6 +11,8 @@ the buffer. import sys import ctypes +from functools import lru_cache + import pyglet from pyglet.gl import * @@ -98,36 +100,6 @@ class AbstractBuffer: raise NotImplementedError('abstract') -class AbstractMappable: - - def get_region(self, start, size, ptr_type): - """Map a region of the buffer into a ctypes array of the desired - type. This region does not need to be unmapped, but will become - invalid if the buffer is resized. - - Note that although a pointer type is required, an array is mapped. - For example:: - - get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20)) - - will map bytes 0 to 80 of the buffer to an array of 20 ints. - - Changes to the array may not be recognised until the region's - :py:meth:`AbstractBufferRegion.invalidate` method is called. - - :Parameters: - `start` : int - Offset into the buffer to map from, in bytes - `size` : int - Size of the buffer region to map, in bytes - `ptr_type` : ctypes pointer type - Pointer type describing the array format to create - - :rtype: :py:class:`AbstractBufferRegion` - """ - raise NotImplementedError('abstract') - - class BufferObject(AbstractBuffer): """Lightweight representation of an OpenGL Buffer Object. @@ -180,7 +152,8 @@ class BufferObject(AbstractBuffer): def map(self): glBindBuffer(GL_ARRAY_BUFFER, self.id) - ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY), ctypes.POINTER(ctypes.c_byte * self.size)).contents + ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY), + ctypes.POINTER(ctypes.c_byte * self.size)).contents return ptr def map_range(self, start, size, ptr_type): @@ -191,21 +164,18 @@ class BufferObject(AbstractBuffer): def unmap(self): glUnmapBuffer(GL_ARRAY_BUFFER) - def __del__(self): - try: - if self.id is not None: - self._context.delete_buffer(self.id) - except: - pass - def delete(self): - buffer_id = GLuint(self.id) - try: - glDeleteBuffers(1, buffer_id) - except Exception: - pass + glDeleteBuffers(1, self.id) self.id = None + def __del__(self): + if self.id is not None: + try: + self._context.delete_buffer(self.id) + self.id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down + def resize(self, size): # Map, create a copy, then reinitialize. temp = (ctypes.c_byte * size)() @@ -222,27 +192,31 @@ class BufferObject(AbstractBuffer): return f"{self.__class__.__name__}(id={self.id}, size={self.size})" -class MappableBufferObject(BufferObject, AbstractMappable): +class AttributeBufferObject(BufferObject): """A buffer with system-memory backed store. - Updates to the data via `set_data`, `set_data_region` and `map` will be - held in local memory until `bind` is called. The advantage is that fewer - OpenGL calls are needed, increasing performance. - - There may also be less performance penalty for resizing this buffer. - - Updates to data via :py:meth:`map` are committed immediately. + Updates to the data via `set_data` and `set_data_region` will be held + in local memory until `buffer_data` is called. The advantage is that + fewer OpenGL calls are needed, which can increasing performance at the + expense of system memory. """ - def __init__(self, size, usage=GL_DYNAMIC_DRAW): - super(MappableBufferObject, self).__init__(size, usage) + + def __init__(self, size, attribute, usage=GL_DYNAMIC_DRAW): + super().__init__(size, usage) + self._size = size self.data = (ctypes.c_byte * size)() self.data_ptr = ctypes.addressof(self.data) self._dirty_min = sys.maxsize self._dirty_max = 0 - def bind(self): - # Commit pending data - super(MappableBufferObject, self).bind() + self.attribute_stride = attribute.stride + self.attribute_count = attribute.count + self.attribute_ctype = attribute.c_type + + self._array = self.get_region(0, size).array + + def bind(self, target=GL_ARRAY_BUFFER): + super().bind(target) size = self._dirty_max - self._dirty_min if size > 0: if size == self.size: @@ -252,28 +226,27 @@ class MappableBufferObject(BufferObject, AbstractMappable): self._dirty_min = sys.maxsize self._dirty_max = 0 - def set_data(self, data): - super(MappableBufferObject, self).set_data(data) - ctypes.memmove(self.data, data, self.size) - self._dirty_min = 0 - self._dirty_max = self.size + @lru_cache(maxsize=None) + def get_region(self, start, count): + byte_start = self.attribute_stride * start # byte offset + byte_size = self.attribute_stride * count # number of bytes + array_count = self.attribute_count * count # number of values - def set_data_region(self, data, start, length): - ctypes.memmove(self.data_ptr + start, data, length) - self._dirty_min = min(start, self._dirty_min) - self._dirty_max = max(start + length, self._dirty_max) + ptr_type = ctypes.POINTER(self.attribute_ctype * array_count) + array = ctypes.cast(self.data_ptr + byte_start, ptr_type).contents + return BufferObjectRegion(self, byte_start, byte_start + byte_size, array) - def map(self, invalidate=False): - self._dirty_min = 0 - self._dirty_max = self.size - return self.data + def set_region(self, start, count, data): + byte_start = self.attribute_stride * start # byte offset + byte_size = self.attribute_stride * count # number of bytes - def unmap(self): - pass + array_start = start * self.attribute_count + array_end = count * self.attribute_count + array_start - def get_region(self, start, size, ptr_type): - array = ctypes.cast(self.data_ptr + start, ptr_type).contents - return BufferObjectRegion(self, start, start + size, array) + self._array[array_start:array_end] = data + + self._dirty_min = min(self._dirty_min, byte_start) + self._dirty_max = max(self._dirty_max, byte_start + byte_size) def resize(self, size): data = (ctypes.c_byte * size)() @@ -289,6 +262,9 @@ class MappableBufferObject(BufferObject, AbstractMappable): self._dirty_min = sys.maxsize self._dirty_max = 0 + self._array = self.get_region(0, size).array + self.get_region.cache_clear() + class BufferObjectRegion: """A mapped region of a MappableBufferObject.""" diff --git a/libs/pyglet/graphics/vertexdomain.py b/libs/pyglet/graphics/vertexdomain.py index 8723d13..ff55f47 100644 --- a/libs/pyglet/graphics/vertexdomain.py +++ b/libs/pyglet/graphics/vertexdomain.py @@ -23,11 +23,9 @@ primitives of the same OpenGL primitive mode. import ctypes -import pyglet - from pyglet.gl import * from pyglet.graphics import allocation, shader, vertexarray -from pyglet.graphics.vertexbuffer import BufferObject, MappableBufferObject +from pyglet.graphics.vertexbuffer import BufferObject, AttributeBufferObject def _nearest_pow2(v): @@ -66,22 +64,39 @@ _gl_types = { } +def _make_attribute_property(name): + + def _attribute_getter(self): + attribute = self.domain.attribute_names[name] + region = attribute.buffer.get_region(self.start, self.count) + region.invalidate() + return region.array + + def _attribute_setter(self, data): + attribute = self.domain.attribute_names[name] + attribute.buffer.set_region(self.start, self.count, data) + + return property(_attribute_getter, _attribute_setter) + + + class VertexDomain: """Management of a set of vertex lists. Construction of a vertex domain is usually done with the :py:func:`create_domain` function. """ - version = 0 _initial_count = 16 def __init__(self, program, attribute_meta): - self.program = program + self.program = program # Needed a reference for migration self.attribute_meta = attribute_meta self.allocator = allocation.Allocator(self._initial_count) - self.attributes = [] - self.buffer_attributes = [] # list of (buffer, attributes) + self.attribute_names = {} # name: attribute + self.buffer_attributes = [] # list of (buffer, attributes) + + self._property_dict = {} # name: property(_getter, _setter) for name, meta in attribute_meta.items(): assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'." @@ -90,14 +105,19 @@ class VertexDomain: gl_type = _gl_types[meta['format'][0]] normalize = 'n' in meta['format'] attribute = shader.Attribute(name, location, count, gl_type, normalize) - self.attributes.append(attribute) + self.attribute_names[attribute.name] = attribute # Create buffer: - attribute.buffer = MappableBufferObject(attribute.stride * self.allocator.capacity) - attribute.buffer.element_size = attribute.stride - attribute.buffer.attributes = (attribute,) + attribute.buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity, attribute) + self.buffer_attributes.append((attribute.buffer, (attribute,))) + # Create custom property to be used in the VertexList: + self._property_dict[attribute.name] = _make_attribute_property(name) + + # Make a custom VertexList class w/ properties for each attribute in the ShaderProgram: + self._vertexlist_class = type("VertexList", (VertexList,), self._property_dict) + self.vao = vertexarray.VertexArray() self.vao.bind() for buffer, attributes in self.buffer_attributes: @@ -107,29 +127,14 @@ class VertexDomain: attribute.set_pointer(buffer.ptr) self.vao.unbind() - # Create named attributes for each attribute - self.attribute_names = {} - for attribute in self.attributes: - self.attribute_names[attribute.name] = attribute - - def __del__(self): - # Break circular refs that Python GC seems to miss even when forced - # collection. - for attribute in self.attributes: - try: - del attribute.buffer - except AttributeError: - pass - def safe_alloc(self, count): """Allocate vertices, resizing the buffers if necessary.""" try: return self.allocator.alloc(count) except allocation.AllocatorMemoryException as e: capacity = _nearest_pow2(e.requested_capacity) - self.version += 1 for buffer, _ in self.buffer_attributes: - buffer.resize(capacity * buffer.element_size) + buffer.resize(capacity * buffer.attribute_stride) self.allocator.set_capacity(capacity) return self.allocator.alloc(count) @@ -139,9 +144,8 @@ class VertexDomain: return self.allocator.realloc(start, count, new_count) except allocation.AllocatorMemoryException as e: capacity = _nearest_pow2(e.requested_capacity) - self.version += 1 for buffer, _ in self.buffer_attributes: - buffer.resize(capacity * buffer.element_size) + buffer.resize(capacity * buffer.attribute_stride) self.allocator.set_capacity(capacity) return self.allocator.realloc(start, count, new_count) @@ -157,7 +161,7 @@ class VertexDomain: :rtype: :py:class:`VertexList` """ start = self.safe_alloc(count) - return VertexList(self, start, count) + return self._vertexlist_class(self, start, count) def draw(self, mode): """Draw all vertices in the domain. @@ -221,8 +225,6 @@ class VertexList: self.domain = domain self.start = start self.count = count - self._caches = {} - self._cache_versions = {} def draw(self, mode): """Draw this vertex list in the given OpenGL mode. @@ -247,7 +249,7 @@ class VertexList: new_start = self.domain.safe_realloc(self.start, self.count, count) if new_start != self.start: # Copy contents to new location - for attribute in self.domain.attributes: + for attribute in self.domain.attribute_names.values(): old = attribute.get_region(attribute.buffer, self.start, self.count) new = attribute.get_region(attribute.buffer, new_start, self.count) new.array[:] = old.array[:] @@ -255,9 +257,6 @@ class VertexList: self.start = new_start self.count = count - for version in self._cache_versions: - self._cache_versions[version] = None - def delete(self): """Delete this group.""" self.domain.allocator.dealloc(self.start, self.count) @@ -287,33 +286,10 @@ class VertexList: self.domain = domain self.start = new_start - for version in self._cache_versions: - self._cache_versions[version] = None - def set_attribute_data(self, name, data): attribute = self.domain.attribute_names[name] attribute.set_region(attribute.buffer, self.start, self.count, data) - def __getattr__(self, name): - """dynamic access to vertex attributes, for backwards compatibility. - """ - domain = self.domain - if self._cache_versions.get(name, None) != domain.version: - attribute = domain.attribute_names[name] - self._caches[name] = attribute.get_region(attribute.buffer, self.start, self.count) - self._cache_versions[name] = domain.version - - region = self._caches[name] - region.invalidate() - return region.array - - def __setattr__(self, name, value): - # Allow setting vertex attributes directly without overwriting them: - if 'domain' in self.__dict__ and name in self.__dict__['domain'].attribute_names: - getattr(self, name)[:] = value - return - super().__setattr__(name, value) - class IndexedVertexDomain(VertexDomain): """Management of a set of indexed vertex lists. @@ -337,13 +313,15 @@ class IndexedVertexDomain(VertexDomain): self.index_buffer.bind_to_index_buffer() self.vao.unbind() + # Make a custom VertexList class w/ properties for each attribute in the ShaderProgram: + self._vertexlist_class = type("IndexedVertexList", (IndexedVertexList,), self._property_dict) + def safe_index_alloc(self, count): """Allocate indices, resizing the buffers if necessary.""" try: return self.index_allocator.alloc(count) except allocation.AllocatorMemoryException as e: capacity = _nearest_pow2(e.requested_capacity) - self.version += 1 self.index_buffer.resize(capacity * self.index_element_size) self.index_allocator.set_capacity(capacity) return self.index_allocator.alloc(count) @@ -354,7 +332,6 @@ class IndexedVertexDomain(VertexDomain): return self.index_allocator.realloc(start, count, new_count) except allocation.AllocatorMemoryException as e: capacity = _nearest_pow2(e.requested_capacity) - self.version += 1 self.index_buffer.resize(capacity * self.index_element_size) self.index_allocator.set_capacity(capacity) return self.index_allocator.realloc(start, count, new_count) @@ -371,7 +348,7 @@ class IndexedVertexDomain(VertexDomain): """ start = self.safe_alloc(count) index_start = self.safe_index_alloc(index_count) - return IndexedVertexList(self, start, count, index_start, index_count) + return self._vertexlist_class(self, start, count, index_start, index_count) def get_index_region(self, start, count): """Get a data from a region of the index buffer. diff --git a/libs/pyglet/gui/widgets.py b/libs/pyglet/gui/widgets.py index 922746b..f0b5807 100644 --- a/libs/pyglet/gui/widgets.py +++ b/libs/pyglet/gui/widgets.py @@ -433,6 +433,34 @@ class TextEntry(WidgetBase): assert type(value) is str, "This Widget's value must be a string." self._doc.text = value + @property + def width(self): + return self._width + + @width.setter + def width(self, value): + self._width = value + self._layout.width = value + self._outline.width = value + + @property + def height(self): + return self._height + + @height.setter + def height(self, value): + self._height = value + self._layout.height = value + self._outline.height = value + + @property + def focus(self) -> bool: + return self._focus + + @focus.setter + def focus(self, value: bool) -> None: + self._set_focus(value) + def _check_hit(self, x, y): return self._x < x < self._x + self._width and self._y < y < self._y + self._height diff --git a/libs/pyglet/image/__init__.py b/libs/pyglet/image/__init__.py index e9a6431..0ef0592 100644 --- a/libs/pyglet/image/__init__.py +++ b/libs/pyglet/image/__init__.py @@ -1220,11 +1220,20 @@ class Texture(AbstractImage): self.id = tex_id self._context = pyglet.gl.current_context + def delete(self): + """Delete this texture and the memory it occupies. + After this, it may not be used anymore. + """ + glDeleteTextures(1, self.id) + self.id = None + def __del__(self): - try: - self._context.delete_texture(self.id) - except Exception: - pass + if self.id is not None: + try: + self._context.delete_texture(self.id) + self.id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down def bind(self, texture_unit: int = 0): """Bind to a specific Texture Unit by number.""" @@ -1479,8 +1488,13 @@ class TextureRegion(Texture): return "{}(id={}, size={}x{}, owner={}x{})".format(self.__class__.__name__, self.id, self.width, self.height, self.owner.width, self.owner.height) + def delete(self): + """Deleting a TextureRegion has no effect. Operate on the owning + texture instead. + """ + pass + def __del__(self): - # only the owner Texture should handle deletion pass diff --git a/libs/pyglet/image/buffer.py b/libs/pyglet/image/buffer.py index c2ffdbd..7d02f8a 100644 --- a/libs/pyglet/image/buffer.py +++ b/libs/pyglet/image/buffer.py @@ -13,6 +13,7 @@ class Renderbuffer: def __init__(self, width, height, internal_format, samples=1): """Create an instance of a Renderbuffer object.""" + self._context = pyglet.gl.current_context self._id = GLuint() self._width = width self._height = height @@ -49,13 +50,15 @@ class Renderbuffer: def delete(self): glDeleteRenderbuffers(1, self._id) + self._id = None def __del__(self): - try: - glDeleteRenderbuffers(1, self._id) - # Python interpreter is shutting down: - except Exception: - pass + if self._id is not None: + try: + self._context.delete_renderbuffer(self._id.value) + self._id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down def __repr__(self): return "{}(id={})".format(self.__class__.__name__, self._id.value) @@ -71,6 +74,7 @@ class Framebuffer: .. versionadded:: 2.0 """ + self._context = pyglet.gl.current_context self._id = GLuint() glGenFramebuffers(1, self._id) self._attachment_types = 0 @@ -105,10 +109,16 @@ class Framebuffer: self.unbind() def delete(self): - try: - glDeleteFramebuffers(1, self._id) - except Exception: - pass + glDeleteFramebuffers(1, self._id) + self._id = None + + def __del__(self): + if self._id is not None: + try: + self._context.delete_framebuffer(self._id.value) + self._id = None + except (AttributeError, ImportError): + pass # Interpreter is shutting down @property def is_complete(self): @@ -203,12 +213,5 @@ class Framebuffer: self._height = max(renderbuffer.height, self._height) self.unbind() - def __del__(self): - try: - glDeleteFramebuffers(1, self._id) - # Python interpreter is shutting down: - except Exception: - pass - def __repr__(self): return "{}(id={})".format(self.__class__.__name__, self._id.value) diff --git a/libs/pyglet/image/codecs/wic.py b/libs/pyglet/image/codecs/wic.py index 1e7ab11..6ec3041 100644 --- a/libs/pyglet/image/codecs/wic.py +++ b/libs/pyglet/image/codecs/wic.py @@ -601,7 +601,7 @@ class WICEncoder(ImageEncoder): frame.SetPixelFormat(byref(default_format)) - data = (c_byte * size).from_buffer(bytearray(image_data)) + data = (BYTE * size).from_buffer(bytearray(image_data)) frame.WritePixels(image.height, pitch, size, data) diff --git a/libs/pyglet/input/linux/evdev.py b/libs/pyglet/input/linux/evdev.py index 5c00fee..7bda2f3 100644 --- a/libs/pyglet/input/linux/evdev.py +++ b/libs/pyglet/input/linux/evdev.py @@ -4,7 +4,6 @@ import fcntl import ctypes import warnings -from os import readv from ctypes import c_uint16 as _u16 from ctypes import c_int16 as _s16 from ctypes import c_uint32 as _u32 @@ -22,6 +21,8 @@ from pyglet.input.base import Device, RelativeAxis, AbsoluteAxis, Button, Joysti from pyglet.input.base import DeviceOpenException, ControllerManager from pyglet.input.controller import get_mapping, Relation, create_guid +c = pyglet.lib.load_library('c') + _IOC_NRBITS = 8 _IOC_TYPEBITS = 8 _IOC_SIZEBITS = 14 @@ -408,7 +409,7 @@ class EvdevDevice(XlibSelectDevice, Device): return try: - bytes_read = readv(self._fileno, self._event_buffer) + bytes_read = c.read(self._fileno, self._event_buffer, self._event_size) except OSError: self.close() return diff --git a/libs/pyglet/input/win32/xinput.py b/libs/pyglet/input/win32/xinput.py index 73e4958..3fa5102 100644 --- a/libs/pyglet/input/win32/xinput.py +++ b/libs/pyglet/input/win32/xinput.py @@ -113,8 +113,8 @@ ERROR_SUCCESS = 0 class XINPUT_GAMEPAD(Structure): _fields_ = [ ('wButtons', WORD), - ('bLeftTrigger', UBYTE), - ('bRightTrigger', UBYTE), + ('bLeftTrigger', BYTE), + ('bRightTrigger', BYTE), ('sThumbLX', SHORT), ('sThumbLY', SHORT), ('sThumbRX', SHORT), diff --git a/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py b/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py index ad4fb26..40fcf83 100644 --- a/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py +++ b/libs/pyglet/libs/darwin/cocoapy/cocoalibs.py @@ -218,6 +218,7 @@ NSApplicationDidUnhideNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUn NSApplicationDidUpdateNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUpdateNotification') NSPasteboardURLReadingFileURLsOnlyKey = c_void_p.in_dll(appkit, 'NSPasteboardURLReadingFileURLsOnlyKey') NSPasteboardTypeURL = c_void_p.in_dll(appkit, 'NSPasteboardTypeURL') +NSPasteboardTypeString = c_void_p.in_dll(appkit, 'NSPasteboardTypeString') NSDragOperationGeneric = 4 # /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h diff --git a/libs/pyglet/libs/win32/__init__.py b/libs/pyglet/libs/win32/__init__.py index e5c6d7b..f33fc40 100644 --- a/libs/pyglet/libs/win32/__init__.py +++ b/libs/pyglet/libs/win32/__init__.py @@ -211,7 +211,18 @@ _user32.RegisterDeviceNotificationW.restype = HANDLE _user32.RegisterDeviceNotificationW.argtypes = [HANDLE, LPVOID, DWORD] _user32.UnregisterDeviceNotification.restype = BOOL _user32.UnregisterDeviceNotification.argtypes = [HANDLE] - +_user32.SetClipboardData.restype = HANDLE +_user32.SetClipboardData.argtypes = [UINT, HANDLE] +_user32.EmptyClipboard.restype = BOOL +_user32.EmptyClipboard.argtypes = [] +_user32.OpenClipboard.restype = BOOL +_user32.OpenClipboard.argtypes = [HWND] +_user32.CloseClipboard.restype = BOOL +_user32.CloseClipboard.argtypes = [] +_user32.GetClipboardData.restype = HANDLE +_user32.GetClipboardData.argtypes = [UINT] +_user32.SetClipboardData.restype = HANDLE +_user32.SetClipboardData.argtypes = [UINT, HANDLE] # dwmapi _dwmapi.DwmIsCompositionEnabled.restype = c_int diff --git a/libs/pyglet/libs/win32/com.py b/libs/pyglet/libs/win32/com.py index 7c9a3f8..fbfb17e 100644 --- a/libs/pyglet/libs/win32/com.py +++ b/libs/pyglet/libs/win32/com.py @@ -18,7 +18,7 @@ Interfaces can define methods:: ... ] -Only use STDMETHOD or METHOD for the method types (not ordinary ctypes +Only use METHOD, STDMETHOD or VOIDMETHOD for the method types (not ordinary ctypes function types). The 'this' pointer is bound automatically... e.g., call:: device = IDirectSound8() @@ -50,7 +50,7 @@ class GUID(ctypes.Structure): ('Data1', ctypes.c_ulong), ('Data2', ctypes.c_ushort), ('Data3', ctypes.c_ushort), - ('Data4', ctypes.c_ubyte * 8) + ('Data4', ctypes.c_ubyte * 8), ] def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8): @@ -64,11 +64,6 @@ class GUID(ctypes.Structure): return 'GUID(%x, %x, %x, %x, %x, %x, %x, %x, %x, %x, %x)' % ( self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8) - def __cmp__(self, other): - if isinstance(other, GUID): - return ctypes.cmp(bytes(self), bytes(other)) - return -1 - def __eq__(self, other): return isinstance(other, GUID) and bytes(self) == bytes(other) @@ -80,6 +75,10 @@ LPGUID = ctypes.POINTER(GUID) IID = GUID REFIID = ctypes.POINTER(IID) +S_OK = 0x00000000 +E_NOTIMPL = 0x80004001 +E_NOINTERFACE = 0x80004002 + class METHOD: """COM method.""" @@ -88,244 +87,147 @@ class METHOD: self.restype = restype self.argtypes = args - def get_field(self): - # ctypes caches WINFUNCTYPE's so this should be ok. - return ctypes.WINFUNCTYPE(self.restype, *self.argtypes) + self.prototype = ctypes.WINFUNCTYPE(self.restype, *self.argtypes) + self.direct_prototype = ctypes.WINFUNCTYPE(self.restype, ctypes.c_void_p, *self.argtypes) + + def get_com_proxy(self, i, name): + return self.prototype(i, name) class STDMETHOD(METHOD): """COM method with HRESULT return value.""" def __init__(self, *args): - super(STDMETHOD, self).__init__(ctypes.HRESULT, *args) + super().__init__(ctypes.HRESULT, *args) -class COMMethodInstance: - """Binds a COM interface method.""" +class VOIDMETHOD(METHOD): + """COM method with no return value.""" - def __init__(self, name, i, method): - self.name = name - self.i = i - self.method = method - - def __get__(self, obj, tp): - if obj is not None: - def _call(*args): - assert _debug_com('COM: #{} IN {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args)) - ret = self.method.get_field()(self.i, self.name)(obj, *args) - assert _debug_com('COM: #{} OUT {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args)) - assert _debug_com('COM: RETURN {}'.format(ret)) - return ret - - return _call - - raise AttributeError() + def __init__(self, *args): + super().__init__(None, *args) -class COMInterface(ctypes.Structure): - """Dummy struct to serve as the type of all COM pointers.""" - _fields_ = [ - ('lpVtbl', ctypes.c_void_p), - ] +_DummyPointerType = ctypes.POINTER(ctypes.c_int) +_PointerMeta = type(_DummyPointerType) +_StructMeta = type(ctypes.Structure) -class InterfacePtrMeta(type(ctypes.POINTER(COMInterface))): - """Allows interfaces to be subclassed as ctypes POINTER and expects to be populated with data from a COM object. - TODO: Phase this out and properly use POINTER(Interface) where applicable. - """ +class _InterfaceMeta(_StructMeta): + def __new__(cls, name, bases, dct, /, create_pointer_type=True): + if len(bases) > 1: + assert _debug_com(f"Ignoring {len(bases) - 1} bases on {name}") + bases = (bases[0],) + if not '_methods_' in dct: + dct['_methods_'] = () + + inh_methods = [] + if bases[0] is not ctypes.Structure: # Method does not exist for first definition below + for interface_type in (bases[0].get_interface_inheritance()): + inh_methods.extend(interface_type.__dict__['_methods_']) + + inh_methods = tuple(inh_methods) + new_methods = tuple(dct['_methods_']) + + vtbl_own_offset = len(inh_methods) + + all_methods = tuple(inh_methods) + new_methods + for i, (method_name, mt) in enumerate(all_methods): + assert _debug_com(f"{name}[{i}]: {method_name}: " + f"{(', '.join(t.__name__ for t in mt.argtypes) or 'void')} -> " + f"{'void' if mt.restype is None else mt.restype.__name__}") + + vtbl_struct_type = _StructMeta(f"Vtable_{name}", + (ctypes.Structure,), + {'_fields_': [(n, x.direct_prototype) for n, x in all_methods]}) + dct['_vtbl_struct_type'] = vtbl_struct_type + dct['vtbl_own_offset'] = vtbl_own_offset + + dct['_fields_'] = (('vtbl_ptr', ctypes.POINTER(vtbl_struct_type)),) + + res_type = super().__new__(cls, name, bases, dct) + if create_pointer_type: + # If we're not being created from a pInterface subclass as helper Interface (so likely + # being explicitly defined from user code for later use), create the special + # pInterface pointer subclass so it registers itself into the pointer cache + _pInterfaceMeta(f"p{name}", (ctypes.POINTER(bases[0]),), {'_type_': res_type}) + + return res_type + + +class _pInterfaceMeta(_PointerMeta): def __new__(cls, name, bases, dct): - methods = [] - for base in bases[::-1]: - methods.extend(base.__dict__.get('_methods_', ())) - methods.extend(dct.get('_methods_', ())) + # Interfaces can also be declared by inheritance of pInterface subclasses. + # If this happens, create the interface and then become pointer to its struct. - for i, (n, method) in enumerate(methods): - dct[n] = COMMethodInstance(n, i, method) + target = dct.get('_type_', None) + # If we weren't created due to an Interface subclass definition (don't have a _type_), + # just define that Interface subclass from our base's _type_ + if target is None: + interface_base = bases[0]._type_ - dct['_type_'] = COMInterface + # Create corresponding interface type and then set it as target + target = _InterfaceMeta(f"_{name}_HelperInterface", + (interface_base,), + {'_methods_': dct.get('_methods_', ())}, + create_pointer_type=False) + dct['_type_'] = target - return super(InterfacePtrMeta, cls).__new__(cls, name, bases, dct) + # Create method proxies that will forward ourselves into the interface's methods + for i, (method_name, method) in enumerate(target._methods_): + m = method.get_com_proxy(i + target.vtbl_own_offset, method_name) + def pinterface_method_forward(self, *args, _m=m, _i=i): + assert _debug_com(f'Calling COM {_i} of {target.__name__} ({_m}) through ' + f'pointer: ({", ".join(map(repr, (self, *args)))})') + return _m(self, *args) + dct[method_name] = pinterface_method_forward + pointer_type = super().__new__(cls, name, bases, dct) -class pInterface(ctypes.POINTER(COMInterface), metaclass=InterfacePtrMeta): - """Base COM interface pointer.""" - - -class COMInterfaceMeta(type): - """This differs in the original as an implemented interface object, not a POINTER object. - Used when the user must implement their own functions within an interface rather than - being created and generated by the COM object itself. The types are automatically inserted in the ctypes type - cache so it can recognize the type arguments. - """ - - def __new__(mcs, name, bases, dct): - methods = dct.pop("_methods_", None) - cls = type.__new__(mcs, name, bases, dct) - - if methods is not None: - cls._methods_ = methods - - if not bases: - _ptr_bases = (cls, COMPointer) - else: - _ptr_bases = (cls, ctypes.POINTER(bases[0])) - - # Class type is dynamically created inside __new__ based on metaclass inheritence; update ctypes cache manually. + # Hack selves into the ctypes pointer cache so all uses of `ctypes.POINTER` on the + # interface type will yield it instead of the inflexible standard pointer type. + # NOTE: This is done pretty much exclusively to help convert COMObjects. + # Some additional work from callers like + # RegisterCallback(callback_obj.as_interface(ICallback)) + # instead of + # RegisterCallback(callback_obj) + # could make it obsolete. from ctypes import _pointer_type_cache - _pointer_type_cache[cls] = type(COMPointer)("POINTER({})".format(cls.__name__), - _ptr_bases, - {"__interface__": cls}) + _pointer_type_cache[target] = pointer_type - return cls + return pointer_type - def __get_subclassed_methodcount(self): - """Returns the amount of COM methods in all subclasses to determine offset of methods. - Order must be exact from the source when calling COM methods. + +class Interface(ctypes.Structure, metaclass=_InterfaceMeta, create_pointer_type=False): + @classmethod + def get_interface_inheritance(cls): + """Returns the types of all interfaces implemented by this interface, up to but not + including the base `Interface`. + `Interface` does not represent an actual interface, but merely the base concept of + them, so viewing it as part of an interface's inheritance chain is meaningless. """ - try: - result = 0 - for itf in self.mro()[1:-1]: - result += len(itf.__dict__["_methods_"]) - return result - except KeyError as err: - (name,) = err.args - if name == "_methods_": - raise TypeError("Interface '{}' requires a _methods_ attribute.".format(itf.__name__)) - raise + return cls.__mro__[:cls.__mro__.index(Interface)] -class COMPointerMeta(type(ctypes.c_void_p), COMInterfaceMeta): - """Required to prevent metaclass conflicts with inheritance.""" - - -class COMPointer(ctypes.c_void_p, metaclass=COMPointerMeta): - """COM Pointer base, could use c_void_p but need to override from_param .""" +class pInterface(_DummyPointerType, metaclass=_pInterfaceMeta): + _type_ = Interface @classmethod def from_param(cls, obj): - """Allows obj to return ctypes pointers, even if its base is not a ctype. - In this case, all we simply want is a ctypes pointer matching the cls interface from the obj. - """ - if obj is None: - return + """When dealing with a COMObject, pry a fitting interface out of it""" - try: - ptr_dct = obj._pointers - except AttributeError: - raise Exception("Interface method argument specified incorrectly, or passed wrong argument.", cls) - else: - try: - return ptr_dct[cls.__interface__] - except KeyError: - raise TypeError("Interface {} doesn't have a pointer in this class.".format(cls.__name__)) + if not isinstance(obj, COMObject): + return obj + + return obj.as_interface(cls._type_) -def _missing_impl(interface_name, method_name): - """Functions that are not implemented use this to prevent errors when called.""" - - def missing_cb_func(*args): - """Return E_NOTIMPL because the method is not implemented.""" - assert _debug_com("Undefined method: {0} was called in interface: {1}".format(method_name, interface_name)) - return 0 - - return missing_cb_func - - -def _found_impl(interface_name, method_name, method_func): - """If a method was found in class, we can set it as a callback.""" - - def cb_func(*args, **kw): - try: - result = method_func(*args, **kw) - except Exception as err: - raise err - - if not result: # QOL so callbacks don't need to specify a return for assumed OK's. - return 0 - - return result - - return cb_func - - -def _make_callback_func(interface, name, method_func): - """Create a callback function for ctypes if possible.""" - if method_func is None: - return _missing_impl(interface, name) - - return _found_impl(interface, name, method_func) - - -# Store structures with same fields to prevent duplicate table creations. -_cached_structures = {} - - -def create_vtbl_structure(fields, interface): - """Create virtual table structure with fields for use in COM's.""" - try: - return _cached_structures[fields] - except KeyError: - Vtbl = type("Vtbl_{}".format(interface.__name__), (ctypes.Structure,), {"_fields_": fields}) - _cached_structures[fields] = Vtbl - return Vtbl - - -class COMObject: - """A base class for defining a COM object for use with callbacks and custom implementations.""" - _interfaces_ = [] - - def __new__(cls, *args, **kw): - new_cls = super(COMObject, cls).__new__(cls) - assert len(cls._interfaces_) > 0, "Atleast one interface must be defined to use a COMObject." - new_cls._pointers = {} - new_cls.__create_interface_pointers() - return new_cls - - def __create_interface_pointers(cls): - """Create a custom ctypes structure to handle COM functions in a COM Object.""" - interfaces = tuple(cls._interfaces_) - for itf in interfaces[::-1]: - methods = [] - fields = [] - for interface in itf.__mro__[-2::-1]: - for method in interface._methods_: - name, com_method = method - - found_method = getattr(cls, name, None) - mth = _make_callback_func(itf.__name__, name, found_method) - - proto = ctypes.WINFUNCTYPE(com_method.restype, *com_method.argtypes) - - fields.append((name, proto)) - methods.append(proto(mth)) - - # Make a structure dynamically with the fields given. - itf_structure = create_vtbl_structure(tuple(fields), interface) - - # Assign the methods to the fields - vtbl = itf_structure(*methods) - - cls._pointers[itf] = ctypes.pointer(ctypes.pointer(vtbl)) - - @property - def pointers(self): - """Returns pointers to the implemented interfaces in this COMObject. Read-only. - - :type: dict - """ - return self._pointers - -class Interface(metaclass=COMInterfaceMeta): - _methods_ = [] - - -class IUnknown(metaclass=COMInterfaceMeta): - """These methods are not implemented by default yet. Strictly for COM method ordering.""" +class IUnknown(Interface): _methods_ = [ - ('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)), - ('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)), - ('Release', METHOD(ctypes.c_int, ctypes.c_void_p)) + ('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)), + ('AddRef', METHOD(ctypes.c_int)), + ('Release', METHOD(ctypes.c_int)), ] @@ -333,5 +235,163 @@ class pIUnknown(pInterface): _methods_ = [ ('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)), ('AddRef', METHOD(ctypes.c_int)), - ('Release', METHOD(ctypes.c_int)) + ('Release', METHOD(ctypes.c_int)), ] + + +def _missing_impl(interface_name, method_name): + """Create a callback returning E_NOTIMPL for methods not present on a COMObject.""" + + def missing_cb_func(*_): + assert _debug_com(f"Non-implemented method {method_name} called in {interface_name}") + return E_NOTIMPL + + return missing_cb_func + + +def _found_impl(interface_name, method_name, method_func, self_distance): + """If a method was found in class, create a callback extracting self from the struct + pointer. + """ + + def self_extracting_cb_func(p, *args): + assert _debug_com(f"COMObject method {method_name} called through interface {interface_name}") + self = ctypes.cast(p + self_distance, ctypes.POINTER(ctypes.py_object)).contents.value + result = method_func(self, *args) + # Assume no return statement translates to success + return S_OK if result is None else result + + return self_extracting_cb_func + + +def _adjust_impl(interface_name, method_name, original_method, offset): + """A method implemented in a previous interface modifies the COMOboject pointer so it + corresponds to an earlier interface and passes it on to the actual implementation. + """ + + def adjustor_cb_func(p, *args): + assert _debug_com(f"COMObject method {method_name} called through interface " + f"{interface_name}, adjusting pointer by {offset}") + return original_method(p + offset, *args) + + return adjustor_cb_func + + +class COMObject: + """A COMObject for implementing C callbacks in Python. + Specify the interface types it supports in `_interfaces_`, and any methods to be implemented + by those interfaces as standard python methods. If the names match, they will be run as + callbacks with all arguments supplied as the types specified in the corresponding interface, + and `self` available as usual. + Remember to call `super().__init__()`. + + COMObjects can be passed to ctypes functions directly as long as the corresponding argtype is + an `Interface` pointer, or a `pInterface` subclass. + + IUnknown's methods will be autogenerated in case IUnknown is implemented. + """ + + def __init_subclass__(cls, /, **kwargs): + super().__init_subclass__(**kwargs) + + implemented_leaf_interfaces = cls.__dict__.get('_interfaces_', ()) + if not implemented_leaf_interfaces: + raise TypeError("At least one interface must be defined to use a COMObject") + + for interface_type in implemented_leaf_interfaces: + for other in implemented_leaf_interfaces: + if interface_type is other: + continue + if issubclass(interface_type, other): + raise TypeError("Only specify the leaf interfaces") + + # Sanity check done + + _ptr_size = ctypes.sizeof(ctypes.c_void_p) + + _vtbl_pointers = [] + implemented_methods = {} + + # Map all leaf and inherited interfaces to the offset of the vtable containing + # their implementations + _interface_to_vtbl_offset = {} + for i, interface_type in enumerate(implemented_leaf_interfaces): + bases = interface_type.get_interface_inheritance() + for base in bases: + if base not in _interface_to_vtbl_offset: + _interface_to_vtbl_offset[base] = i * _ptr_size + + if IUnknown in _interface_to_vtbl_offset: + def QueryInterface(self, iid_ptr, res_ptr): + ctypes.cast(res_ptr, ctypes.POINTER(ctypes.c_void_p))[0] = 0 + return E_NOINTERFACE + + def AddRef(self): + self._vrefcount += 1 + return self._vrefcount + + def Release(self): + if self._vrefcount <= 0: + assert _debug_com( + f"COMObject {self}: Release while refcount was {self._vrefcount}" + ) + self._vrefcount -= 1 + return self._vrefcount + + cls.QueryInterface = QueryInterface + cls.AddRef = AddRef + cls.Release = Release + + for i, interface_type in enumerate(implemented_leaf_interfaces): + wrappers = [] + + for method_name, method_type in interface_type._vtbl_struct_type._fields_: + if method_name in implemented_methods: + # Method is already implemented on a previous interface; redirect to it + # See https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723 + # NOTE: Never tested, might be totally wrong + func, implementing_vtbl_idx = implemented_methods[method_name] + mth = _adjust_impl(interface_type.__name__, + method_name, + func, + (implementing_vtbl_idx - i) * _ptr_size) + + else: + if (found_method := getattr(cls, method_name, None)) is None: + mth = _missing_impl(interface_type.__name__, method_name) + else: + mth = _found_impl(interface_type.__name__, + method_name, + found_method, + (len(implemented_leaf_interfaces) - i) * _ptr_size) + + implemented_methods[method_name] = (mth, i) + + wrappers.append(method_type(mth)) + + vtbl = interface_type._vtbl_struct_type(*wrappers) + _vtbl_pointers.append(ctypes.pointer(vtbl)) + + fields = [] + for i, itf in enumerate(implemented_leaf_interfaces): + fields.append((f'vtbl_ptr_{i}', ctypes.POINTER(itf._vtbl_struct_type))) + fields.append(('self_', ctypes.py_object)) + + cls._interface_to_vtbl_offset = _interface_to_vtbl_offset + cls._vtbl_pointers = _vtbl_pointers + cls._struct_type = _StructMeta(f"{cls.__name__}_Struct", (ctypes.Structure,), {'_fields_': fields}) + + def __init__(self): + self._vrefcount = 1 + self._struct = self._struct_type(*self._vtbl_pointers, ctypes.py_object(self)) + + def as_interface(self, interface_type): + # This method ignores the QueryInterface mechanism completely; no GUIDs are + # associated with Interfaces on the python side, it can't be supported. + # Still works, as so far none of the python-made COMObjects are expected to + # support it by any C code. + # (Also no need to always implement it, some COMObjects do not inherit from IUnknown.) + if (offset := self._interface_to_vtbl_offset.get(interface_type, None)) is None: + raise TypeError(f"Does not implement {interface_type}") + + return ctypes.byref(self._struct, offset) diff --git a/libs/pyglet/libs/win32/types.py b/libs/pyglet/libs/win32/types.py index 097c32b..d7667cc 100644 --- a/libs/pyglet/libs/win32/types.py +++ b/libs/pyglet/libs/win32/types.py @@ -1,4 +1,5 @@ import ctypes +import sys from ctypes import * from ctypes.wintypes import * @@ -45,7 +46,6 @@ def POINTER_(obj): c_void_p = POINTER_(c_void) INT = c_int -UBYTE = c_ubyte LPVOID = c_void_p HCURSOR = HANDLE LRESULT = LPARAM @@ -62,6 +62,11 @@ HDROP = HANDLE LPTSTR = LPWSTR LPSTREAM = c_void_p +# Fixed in python 3.12. Is c_byte on other versions. +# Ensure it's the same across all versions. +if sys.version_info < (3, 12): + BYTE = c_ubyte + LF_FACESIZE = 32 CCHDEVICENAME = 32 CCHFORMNAME = 32 @@ -572,6 +577,7 @@ class IStream(com.pIUnknown): com.STDMETHOD()), ] + class DEV_BROADCAST_HDR(Structure): _fields_ = ( ('dbch_size', DWORD), @@ -579,6 +585,7 @@ class DEV_BROADCAST_HDR(Structure): ('dbch_reserved', DWORD), ) + class DEV_BROADCAST_DEVICEINTERFACE(Structure): _fields_ = ( ('dbcc_size', DWORD), diff --git a/libs/pyglet/math.py b/libs/pyglet/math.py index 596932b..f6a8320 100644 --- a/libs/pyglet/math.py +++ b/libs/pyglet/math.py @@ -26,6 +26,7 @@ from collections.abc import Iterator as _Iterator number = _typing.Union[float, int] +Mat3T = _typing.TypeVar("Mat3T", bound="Mat3") Mat4T = _typing.TypeVar("Mat4T", bound="Mat4") @@ -627,7 +628,7 @@ class Mat3(tuple): the "@" operator. """ - def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3: + def __new__(cls: type[Mat3T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3T: """Create a 3x3 Matrix A Mat3 can be created with a list or tuple of 9 values. @@ -639,7 +640,7 @@ class Mat3(tuple): `values` : tuple of float or int A tuple or list containing 9 floats or ints. """ - new = super().__new__(Mat3, values) + new = super().__new__(cls, values) assert len(new) == 9, "A 3x3 Matrix requires 9 values" return new @@ -720,10 +721,10 @@ class Mat3(tuple): class Mat4(tuple): - def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0,)) -> Mat4: + def __new__(cls: type[Mat4T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0,)) -> Mat4T: """Create a 4x4 Matrix. `Mat4` is an immutable 4x4 Matrix, which includs most common @@ -738,7 +739,7 @@ class Mat4(tuple): .. note:: Matrix multiplication is performed using the "@" operator. """ - new = super().__new__(Mat4, values) + new = super().__new__(cls, values) assert len(new) == 16, "A 4x4 Matrix requires 16 values" return new @@ -1011,3 +1012,106 @@ class Mat4(tuple): def __repr__(self) -> str: return f"{self.__class__.__name__}{self[0:4]}\n {self[4:8]}\n {self[8:12]}\n {self[12:16]}" + + +class Quaternion(tuple): + """Quaternion""" + + def __new__(cls, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Quaternion: + return super().__new__(Quaternion, (w, x, y, z)) + + @classmethod + def from_mat3(cls) -> Quaternion: + raise NotImplementedError("Not yet implemented") + + @classmethod + def from_mat4(cls) -> Quaternion: + raise NotImplementedError("Not yet implemented") + + def to_mat4(self) -> Mat4: + w = self.w + x = self.x + y = self.y + z = self.z + + a = 1 - (y ** 2 + z ** 2) * 2 + b = 2 * (x * y - z * w) + c = 2 * (x * z + y * w) + + e = 2 * (x * y + z * w) + f = 1 - (x ** 2 + z ** 2) * 2 + g = 2 * (y * z - x * w) + + i = 2 * (x * z - y * w) + j = 2 * (y * z + x * w) + k = 1 - (x ** 2 + y ** 2) * 2 + + # a, b, c, - + # e, f, g, - + # i, j, k, - + # -, -, -, - + + return Mat4((a, b, c, 0.0, e, f, g, 0.0, i, j, k, 0.0, 0.0, 0.0, 0.0, 1.0)) + + def to_mat3(self) -> Mat3: + w = self.w + x = self.x + y = self.y + z = self.z + + a = 1 - (y ** 2 + z ** 2) * 2 + b = 2 * (x * y - z * w) + c = 2 * (x * z + y * w) + + e = 2 * (x * y + z * w) + f = 1 - (x ** 2 + z ** 2) * 2 + g = 2 * (y * z - x * w) + + i = 2 * (x * z - y * w) + j = 2 * (y * z + x * w) + k = 1 - (x ** 2 + y ** 2) * 2 + + # a, b, c, - + # e, f, g, - + # i, j, k, - + # -, -, -, - + + return Mat3((a, b, c, e, f, g, i, j, k)) + + @property + def w(self) -> float: + return self[0] + + @property + def x(self) -> float: + return self[1] + + @property + def y(self) -> float: + return self[2] + + @property + def z(self) -> float: + return self[3] + + def conjugate(self) -> Quaternion: + return Quaternion(self.w, -self.x, -self.y, -self.z) + + @property + def mag(self) -> float: + return self.__abs__() + + def normalize(self) -> Quaternion: + m = self.__abs__() + if m == 0: + return self + return Quaternion(self[0] / m, self[1] / m, self[2] / m, self[3] / m) + + def __abs__(self) -> float: + return _math.sqrt(self.w ** 2 + self.x ** 2 + self.y ** 2 + self.z ** 2) + + def __invert__(self) -> Quaternion: + raise NotImplementedError("Not yet implemented") + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(w={self[0]}, x={self[1]}, y={self[2]}, z={self[3]})" diff --git a/libs/pyglet/media/codecs/ffmpeg.py b/libs/pyglet/media/codecs/ffmpeg.py index 33cf9a8..636ddec 100644 --- a/libs/pyglet/media/codecs/ffmpeg.py +++ b/libs/pyglet/media/codecs/ffmpeg.py @@ -1,6 +1,6 @@ """Use ffmpeg to decode audio and video media. """ - +import sys from collections import deque from ctypes import (c_int, c_int32, c_uint8, c_char_p, addressof, byref, cast, POINTER, Structure, create_string_buffer, memmove) @@ -8,7 +8,7 @@ from ctypes import (c_int, c_int32, c_uint8, c_char_p, import pyglet import pyglet.lib from pyglet import image -from pyglet.util import asbytes, asbytes_filename, asstr +from pyglet.util import asbytes, asstr from . import MediaDecoder from .base import AudioData, SourceInfo, StaticSource from .base import StreamingSource, VideoFormat, AudioFormat @@ -510,10 +510,12 @@ class FFmpegSource(StreamingSource): self._file = None self._memory_file = None + encoded_filename = filename.encode(sys.getfilesystemencoding()) + if file: - self._file, self._memory_file = ffmpeg_open_memory_file(asbytes_filename(filename), file) + self._file, self._memory_file = ffmpeg_open_memory_file(encoded_filename, file) else: - self._file = ffmpeg_open_filename(asbytes_filename(filename)) + self._file = ffmpeg_open_filename(encoded_filename) if not self._file: raise FFmpegException('Could not open "{0}"'.format(filename)) diff --git a/libs/pyglet/media/codecs/wmf.py b/libs/pyglet/media/codecs/wmf.py index 9509eac..88915cc 100644 --- a/libs/pyglet/media/codecs/wmf.py +++ b/libs/pyglet/media/codecs/wmf.py @@ -520,9 +520,9 @@ class WMFSource(Source): imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed)) if guid_compressed == MFAudioFormat_PCM or guid_compressed == MFAudioFormat_Float: - assert _debug('WMFAudioDecoder: Found Uncompressed Audio:', guid_compressed) + assert _debug(f'WMFAudioDecoder: Found Uncompressed Audio: {guid_compressed}') else: - assert _debug('WMFAudioDecoder: Found Compressed Audio:', guid_compressed) + assert _debug(f'WMFAudioDecoder: Found Compressed Audio: {guid_compressed}') # If audio is compressed, attempt to decompress it by forcing source reader to use PCM mf_mediatype = IMFMediaType() diff --git a/libs/pyglet/media/devices/win32.py b/libs/pyglet/media/devices/win32.py index d11acbd..5d42013 100644 --- a/libs/pyglet/media/devices/win32.py +++ b/libs/pyglet/media/devices/win32.py @@ -83,15 +83,15 @@ IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde, class IMMNotificationClient(com.IUnknown): _methods_ = [ ('OnDeviceStateChanged', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)), + com.STDMETHOD(LPCWSTR, DWORD)), ('OnDeviceAdded', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)), + com.STDMETHOD(LPCWSTR)), ('OnDeviceRemoved', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)), + com.STDMETHOD(LPCWSTR)), ('OnDefaultDeviceChanged', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)), + com.STDMETHOD(EDataFlow, ERole, LPCWSTR)), ('OnPropertyValueChanged', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, PROPERTYKEY)), + com.STDMETHOD(LPCWSTR, PROPERTYKEY)), ] @@ -113,7 +113,7 @@ class AudioNotificationCB(com.COMObject): self.audio_devices = audio_devices self._lost = False - def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState): + def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState): device = self.audio_devices.get_cached_device(pwstrDeviceId) old_state = device.state @@ -126,17 +126,17 @@ class AudioNotificationCB(com.COMObject): device.state = dwNewState self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state) - def OnDeviceAdded(self, this, pwstrDeviceId): + def OnDeviceAdded(self, pwstrDeviceId): dev = self.audio_devices.add_device(pwstrDeviceId) assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}") self.audio_devices.dispatch_event('on_device_added', dev) - def OnDeviceRemoved(self, this, pwstrDeviceId): + def OnDeviceRemoved(self, pwstrDeviceId): dev = self.audio_devices.remove_device(pwstrDeviceId) assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}") self.audio_devices.dispatch_event('on_device_removed', dev) - def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId): + def OnDefaultDeviceChanged(self, flow, role, pwstrDeviceId): # Only support eConsole role right now if role == 0: if pwstrDeviceId is None: @@ -149,7 +149,7 @@ class AudioNotificationCB(com.COMObject): self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow) - def OnPropertyValueChanged(self, this, pwstrDeviceId, key): + def OnPropertyValueChanged(self, pwstrDeviceId, key): pass @@ -259,7 +259,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager): cached_dev.state = dev_state return cached_dev except OSError as err: - assert _debug("No default audio output was found.", err) + assert _debug(f"No default audio output was found. {err}") return None def get_default_input(self) -> Optional[Win32AudioDevice]: @@ -274,7 +274,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager): cached_dev.state = dev_state return cached_dev except OSError as err: - assert _debug("No default input output was found.", err) + assert _debug(f"No default input output was found. {err}") return None def get_cached_device(self, dev_id) -> Win32AudioDevice: diff --git a/libs/pyglet/media/drivers/directsound/adaptation.py b/libs/pyglet/media/drivers/directsound/adaptation.py index 3ea2d10..55be0bc 100644 --- a/libs/pyglet/media/drivers/directsound/adaptation.py +++ b/libs/pyglet/media/drivers/directsound/adaptation.py @@ -145,11 +145,11 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer): def _refill(self, write_size): while write_size > 0: - assert _debug('_refill, write_size =', write_size) + assert _debug(f'_refill, write_size = {write_size}') audio_data = self._get_audiodata() if audio_data is not None: - assert _debug('write', audio_data.length) + assert _debug(f'write {audio_data.length}') length = min(write_size, audio_data.length) self.write(audio_data, length) write_size -= length @@ -191,14 +191,14 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer): # Set the write cursor back to eos_cursor or play_cursor to prevent gaps if self._play_cursor < self._eos_cursor: cursor_diff = self._write_cursor - self._eos_cursor - assert _debug('Moving cursor back', cursor_diff) + assert _debug(f'Moving cursor back {cursor_diff}') self._write_cursor = self._eos_cursor self._write_cursor_ring -= cursor_diff self._write_cursor_ring %= self._buffer_size else: cursor_diff = self._play_cursor - self._eos_cursor - assert _debug('Moving cursor back', cursor_diff) + assert _debug(f'Moving cursor back {cursor_diff}') self._write_cursor = self._play_cursor self._write_cursor_ring -= cursor_diff self._write_cursor_ring %= self._buffer_size @@ -207,7 +207,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer): for event in audio_data.events: event_cursor = self._write_cursor + event.timestamp * \ self.source.audio_format.bytes_per_second - assert _debug('Adding event', event, 'at', event_cursor) + assert _debug(f'Adding event {event} at {event_cursor}') self._events.append((event_cursor, event)) def _add_audiodata_timestamp(self, audio_data): diff --git a/libs/pyglet/media/drivers/directsound/lib_dsound.py b/libs/pyglet/media/drivers/directsound/lib_dsound.py index 9f9e438..224299d 100644 --- a/libs/pyglet/media/drivers/directsound/lib_dsound.py +++ b/libs/pyglet/media/drivers/directsound/lib_dsound.py @@ -270,7 +270,6 @@ class IDirectSound(com.pIUnknown): ('Initialize', com.STDMETHOD(com.LPGUID)), ] - _type_ = com.COMInterface DirectSoundCreate = lib.DirectSoundCreate DirectSoundCreate.argtypes = \ diff --git a/libs/pyglet/media/drivers/openal/adaptation.py b/libs/pyglet/media/drivers/openal/adaptation.py index c80a021..eda02f9 100644 --- a/libs/pyglet/media/drivers/openal/adaptation.py +++ b/libs/pyglet/media/drivers/openal/adaptation.py @@ -245,7 +245,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer): return False def _refill(self, write_size): - assert _debug('_refill', write_size) + assert _debug(f'_refill {write_size}') while write_size > self.min_buffer_size: audio_data = self._get_audiodata() diff --git a/libs/pyglet/media/drivers/pulse/adaptation.py b/libs/pyglet/media/drivers/pulse/adaptation.py index 71e302d..3aa3a9b 100644 --- a/libs/pyglet/media/drivers/pulse/adaptation.py +++ b/libs/pyglet/media/drivers/pulse/adaptation.py @@ -236,7 +236,7 @@ class PulseAudioPlayer(AbstractAudioPlayer): while self._events and self._events[0][0] <= read_index: _, event = self._events.pop(0) - assert _debug('PulseAudioPlayer: Dispatch event', event) + assert _debug(f'PulseAudioPlayer: Dispatch event {event}') event.sync_dispatch_to_player(self.player) def _add_event_at_write_index(self, event_name): @@ -313,7 +313,7 @@ class PulseAudioPlayer(AbstractAudioPlayer): else: read_index = 0 - assert _debug('_get_read_index ->', read_index) + assert _debug(f'_get_read_index -> {read_index}') return read_index def _get_write_index(self): @@ -323,7 +323,7 @@ class PulseAudioPlayer(AbstractAudioPlayer): else: write_index = 0 - assert _debug('_get_write_index ->', write_index) + assert _debug(f'_get_write_index -> {write_index}') return write_index def _get_timing_info(self): @@ -365,7 +365,7 @@ class PulseAudioPlayer(AbstractAudioPlayer): dt /= 1000000 time = timestamp + dt - assert _debug('get_time ->', time) + assert _debug('get_time -> {time}') return time def set_volume(self, volume): diff --git a/libs/pyglet/media/drivers/xaudio2/adaptation.py b/libs/pyglet/media/drivers/xaudio2/adaptation.py index decfd36..67b92e2 100644 --- a/libs/pyglet/media/drivers/xaudio2/adaptation.py +++ b/libs/pyglet/media/drivers/xaudio2/adaptation.py @@ -213,7 +213,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer): def _add_audiodata_events(self, audio_data): for event in audio_data.events: event_cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second - assert _debug('Adding event', event, 'at', event_cursor) + assert _debug(f'Adding event {event} at {event_cursor}') self._events.append((event_cursor, event)) def _add_audiodata_timestamp(self, audio_data): diff --git a/libs/pyglet/media/drivers/xaudio2/lib_xaudio2.py b/libs/pyglet/media/drivers/xaudio2/lib_xaudio2.py index 3ee7f30..1392142 100644 --- a/libs/pyglet/media/drivers/xaudio2/lib_xaudio2.py +++ b/libs/pyglet/media/drivers/xaudio2/lib_xaudio2.py @@ -192,17 +192,19 @@ XAUDIO2_NO_VIRTUAL_AUDIO_CLIENT = 0x10000 # Used in CreateMasteringVoice to cr class IXAudio2VoiceCallback(com.Interface): _methods_ = [ ('OnVoiceProcessingPassStart', - com.STDMETHOD(UINT32)), + com.VOIDMETHOD(UINT32)), ('OnVoiceProcessingPassEnd', - com.STDMETHOD()), - ('onStreamEnd', - com.STDMETHOD()), - ('onBufferStart', - com.STDMETHOD(ctypes.c_void_p)), + com.VOIDMETHOD()), + ('OnStreamEnd', + com.VOIDMETHOD()), + ('OnBufferStart', + com.VOIDMETHOD(ctypes.c_void_p)), ('OnBufferEnd', - com.STDMETHOD(ctypes.c_void_p)), + com.VOIDMETHOD(ctypes.c_void_p)), ('OnLoopEnd', - com.STDMETHOD(ctypes.c_void_p)), + com.VOIDMETHOD(ctypes.c_void_p)), + ('OnVoiceError', + com.VOIDMETHOD(ctypes.c_void_p, HRESULT)) ] @@ -220,20 +222,9 @@ class XA2SourceCallback(com.COMObject): _interfaces_ = [IXAudio2VoiceCallback] def __init__(self, xa2_player): + super().__init__() self.xa2_player = xa2_player - def OnVoiceProcessingPassStart(self, bytesRequired): - pass - - def OnVoiceProcessingPassEnd(self): - pass - - def onStreamEnd(self): - pass - - def onBufferStart(self, pBufferContext): - pass - def OnBufferEnd(self, pBufferContext): """At the end of playing one buffer, attempt to refill again. Even if the player is out of sources, it needs to be called to purge all buffers. @@ -241,10 +232,7 @@ class XA2SourceCallback(com.COMObject): if self.xa2_player: self.xa2_player.refill_source_player() - def OnLoopEnd(self, this, pBufferContext): - pass - - def onVoiceError(self, this, pBufferContext, hresult): + def OnVoiceError(self, pBufferContext, hresult): raise Exception("Error occurred during audio playback.", hresult) @@ -362,24 +350,18 @@ class IXAudio2MasteringVoice(IXAudio2Voice): class IXAudio2EngineCallback(com.Interface): _methods_ = [ ('OnProcessingPassStart', - com.METHOD(ctypes.c_void_p)), + com.VOIDMETHOD()), ('OnProcessingPassEnd', - com.METHOD(ctypes.c_void_p)), + com.VOIDMETHOD()), ('OnCriticalError', - com.METHOD(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong)), + com.VOIDMETHOD(HRESULT)), ] class XA2EngineCallback(com.COMObject): _interfaces_ = [IXAudio2EngineCallback] - def OnProcessingPassStart(self): - pass - - def OnProcessingPassEnd(self): - pass - - def OnCriticalError(self, this, hresult): + def OnCriticalError(self, hresult): raise Exception("Critical Error:", hresult) diff --git a/libs/pyglet/shapes.py b/libs/pyglet/shapes.py index 9bc12ae..ca10d72 100644 --- a/libs/pyglet/shapes.py +++ b/libs/pyglet/shapes.py @@ -5,12 +5,11 @@ such as Rectangles, Circles, and Lines. These shapes are made internally from OpenGL primitives, and provide excellent performance when drawn as part of a :py:class:`~pyglet.graphics.Batch`. Convenience methods are provided for positioning, changing color -and opacity, and rotation (where applicable). To create more -complex shapes than what is provided here, the lower level -graphics API is more appropriate. -You can also use the ``in`` operator to check whether a point is -inside a shape. -See the :ref:`guide_graphics` for more details. +and opacity, and rotation (where applicable). +The Python ``in`` operator to check whether a point is inside a shape. + +To create more complex shapes than what is provided here, the lower level +graphics API is more appropriate. See the :ref:`guide_graphics` for more details. A simple example of drawing shapes:: @@ -1466,6 +1465,122 @@ class BorderedRectangle(ShapeBase): self._update_color() +class Box(ShapeBase): + def __init__(self, x, y, width, height, thickness=1, color=(255, 255, 255, 255), batch=None, group=None): + """Create an unfilled rectangular shape, with optional thickness. + + The box's anchor point defaults to the (x, y) coordinates, + which are at the bottom left. + Changing the thickness of the box will extend the walls inward; + the outward dimesions will not be affected. + + :Parameters: + `x` : float + The X coordinate of the box. + `y` : float + The Y coordinate of the box. + `width` : float + The width of the box. + `height` : float + The height of the box. + `thickness` : float + The thickness of the lines that make up the box. + `color` : (int, int, int, int) + The RGB or RGBA color of the box, specified as a tuple + of 3 or 4 ints in the range of 0-255. RGB colors will + be treated as having an opacity of 255. + `batch` : `~pyglet.graphics.Batch` + Optional batch to add the box to. + `group` : `~pyglet.graphics.Group` + Optional parent group of the box. + """ + self._x = x + self._y = y + self._width = width + self._height = height + self._thickness = thickness + + self._num_verts = 8 + + r, g, b, *a = color + self._rgba = r, g, b, a[0] if a else 255 + + program = get_default_shader() + self._batch = batch or Batch() + self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group) + + self._create_vertex_list() + self._update_vertices() + + def __contains__(self, point): + assert len(point) == 2 + point = _rotate_point((self._x, self._y), point, math.radians(self._rotation)) + x, y = self._x - self._anchor_x, self._y - self._anchor_y + return x < point[0] < x + self._width and y < point[1] < y + self._height + + def _create_vertex_list(self): + # 3 6 + # 2 7 + # 1 4 + # 0 5 + indices = [0, 1, 2, 0, 2, 3, 0, 5, 4, 0, 4, 1, 4, 5, 6, 4, 6, 7, 2, 7, 6, 2, 6, 3] + self._vertex_list = self._group.program.vertex_list_indexed( + self._num_verts, self._draw_mode, indices, self._batch, self._group, + colors=('Bn', self._rgba * self._num_verts), + translation=('f', (self._x, self._y) * self._num_verts)) + + def _update_color(self): + self._vertex_list.colors[:] = self._rgba * self._num_verts + + def _update_vertices(self): + if not self._visible: + self._vertex_list.position[:] = (0, 0) * self._num_verts + else: + + t = self._thickness + left = -self._anchor_x + bottom = -self._anchor_y + right = left + self._width + top = bottom + self._height + + x1 = left + x2 = left + t + x3 = right - t + x4 = right + y1 = bottom + y2 = bottom + t + y3 = top - t + y4 = top + # 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + self._vertex_list.position[:] = x1, y1, x2, y2, x2, y3, x1, y4, x3, y2, x4, y1, x4, y4, x3, y3 + + @property + def width(self): + """The width of the Box. + + :type: float + """ + return self._width + + @width.setter + def width(self, value): + self._width = value + self._update_vertices() + + @property + def height(self): + """The height of the Box. + + :type: float + """ + return self._height + + @height.setter + def height(self, value): + self._height = value + self._update_vertices() + + class Triangle(ShapeBase): def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255), batch=None, group=None): @@ -1776,4 +1891,5 @@ class Polygon(ShapeBase): self._vertex_list.position[:] = tuple(value for coordinate in triangles for value in coordinate) -__all__ = 'Arc', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector' +__all__ = ('Arc', 'Box', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle', + 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector', 'ShapeBase') diff --git a/libs/pyglet/text/__init__.py b/libs/pyglet/text/__init__.py index 0772500..68c45e5 100644 --- a/libs/pyglet/text/__init__.py +++ b/libs/pyglet/text/__init__.py @@ -271,6 +271,8 @@ class DocumentLabel(layout.TextLayout): @color.setter def color(self, color): + r, g, b, *a = color + color = r, g, b, a[0] if a else 255 self.document.set_style(0, len(self.document.text), {'color': color}) @property diff --git a/libs/pyglet/text/caret.py b/libs/pyglet/text/caret.py index 5ee69d2..50d86b0 100644 --- a/libs/pyglet/text/caret.py +++ b/libs/pyglet/text/caret.py @@ -98,7 +98,7 @@ class Caret: colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha - self._list = self._group.program.vertex_list(2, gl.GL_LINES, batch, self._group, colors=('Bn', colors)) + self._list = self._group.program.vertex_list(2, gl.GL_LINES, self._batch, self._group, colors=('Bn', colors)) self._ideal_x = None self._ideal_line = None self._next_attributes = {} @@ -127,6 +127,7 @@ class Caret: Also disconnects the caret from further layout events. """ + clock.unschedule(self._blink) self._list.delete() self._layout.remove_handlers(self) diff --git a/libs/pyglet/text/layout.py b/libs/pyglet/text/layout.py index a469374..25d0230 100644 --- a/libs/pyglet/text/layout.py +++ b/libs/pyglet/text/layout.py @@ -540,18 +540,15 @@ class _GlyphBox(_AbstractBox): x1 = x2 if background_vertices: - background_indices = [] - bg_count = len(background_vertices) // 2 + bg_count = len(background_vertices) // 3 + background_indices = [(0, 1, 2, 0, 2, 3)[i % 6] for i in range(bg_count * 3)] decoration_program = get_default_decoration_shader() - for bg_idx in range(bg_count): - background_indices.extend([element + (bg_idx * 4) for element in [0, 1, 2, 0, 2, 3]]) - - background_list = decoration_program.vertex_list_indexed(bg_count * 4, GL_TRIANGLES, background_indices, + background_list = decoration_program.vertex_list_indexed(bg_count, GL_TRIANGLES, background_indices, layout.batch, layout.background_decoration_group, position=('f', background_vertices), colors=('Bn', background_colors), - rotation=('f', (rotation,) * 4), - anchor=('f', (anchor_x, anchor_y) * 4)) + rotation=('f', (rotation,) * bg_count), + anchor=('f', (anchor_x, anchor_y) * bg_count)) context.add_list(background_list) if underline_vertices: diff --git a/libs/pyglet/util.py b/libs/pyglet/util.py index 3d1dbb6..ca8e072 100644 --- a/libs/pyglet/util.py +++ b/libs/pyglet/util.py @@ -3,11 +3,13 @@ import os import sys +from typing import Optional, Union, Callable import pyglet +from pyglet.customtypes import Buffer -def asbytes(s): +def asbytes(s: Union[str, Buffer]) -> bytes: if isinstance(s, bytes): return s elif isinstance(s, str): @@ -16,56 +18,68 @@ def asbytes(s): return bytes(s) -def asbytes_filename(s): - if isinstance(s, bytes): - return s - elif isinstance(s, str): - return s.encode(encoding=sys.getfilesystemencoding()) - - -def asstr(s): +def asstr(s: Optional[Union[str, Buffer]]) -> str: if s is None: return '' if isinstance(s, str): return s - return s.decode("utf-8") + return s.decode("utf-8") # type: ignore -def debug_print(enabled_or_option='debug'): - """Get a debug printer that is enabled based on a boolean input or a pyglet option. - The debug print function returned should be used in an assert. This way it can be - optimized out when running python with the -O flag. +# Keep these outside of the function since we don't need to re-define +# the function each time we make a call since no state is persisted. +def _debug_print_real(arg: str) -> bool: + print(arg) + return True + + +def _debug_print_dummy(arg: str) -> bool: + return True + + +def debug_print(pyglet_option_name: str = 'debug') -> Callable[[str], bool]: + """Get a debug printer controlled by the given ``pyglet.options`` name. + + This allows repurposing ``assert`` to write cleaner, more efficient + debug output: + + #. Debug printers fit into a one-line ``assert`` statements instead + of longer, slower key-lookup ``if`` statements + #. Running Python with the ``-O`` flag makes pyglet run faster by + skipping all ``assert`` statements Usage example:: - from pyglet.debug import debug_print + from pyglet.util import debug_print + + _debug_media = debug_print('debug_media') + def some_func(): + # Python will skip the line below when run with -O assert _debug_media('My debug statement') - :parameters: - `enabled_or_options` : bool or str - If a bool is passed, debug printing is enabled if it is True. If str is passed - debug printing is enabled if the pyglet option with that name is True. + # The rest of the function will run as normal + ... + + For more information, please see `the Python command line + documentation `_. + + Args: + `pyglet_option_name` : + The name of a key in :attr:`pyglet.options` to read the + debug flag's value from. + + Returns: + A callable which prints a passed string and returns ``True`` + to allow auto-removal when running with ``-O``. - :returns: Function for debug printing. """ - if isinstance(enabled_or_option, bool): - enabled = enabled_or_option - else: - enabled = pyglet.options.get(enabled_or_option, False) - + enabled = pyglet.options.get(pyglet_option_name, False) if enabled: - def _debug_print(*args, **kwargs): - print(*args, **kwargs) - return True - - else: - def _debug_print(*args, **kwargs): - return True - - return _debug_print + return _debug_print_real + return _debug_print_dummy class CodecRegistry: diff --git a/libs/pyglet/window/__init__.py b/libs/pyglet/window/__init__.py index 2974c6b..7e3f297 100644 --- a/libs/pyglet/window/__init__.py +++ b/libs/pyglet/window/__init__.py @@ -273,9 +273,10 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): conventions. This will ensure it is not obscured by other windows, and appears on an appropriate screen for the user. - To render into a window, you must first call `switch_to`, to make - it the current OpenGL context. If you use only one window in the - application, there is no need to do this. + To render into a window, you must first call its :py:meth:`.switch_to` + method to make it the active OpenGL context. If you use only one + window in your application, you can skip this step as it will always + be the active context. """ # Filled in by metaclass with the names of all methods on this (sub)class @@ -638,7 +639,8 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): """Clear the window. This is a convenience method for clearing the color and depth - buffer. The window must be the active context (see `switch_to`). + buffer. The window must be the active context (see + :py:meth:`.switch_to`). """ gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) @@ -646,10 +648,12 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): """Close the window. After closing the window, the GL context will be invalid. The - window instance cannot be reused once closed (see also `set_visible`). + window instance cannot be reused once closed. To re-use windows, + see :py:meth:`.set_visible` instead. - The `pyglet.app.EventLoop.on_window_close` event is dispatched on - `pyglet.app.event_loop` when this method is called. + The :py:meth:`pyglet.app.EventLoop.on_window_close` event is + dispatched by the :py:attr:`pyglet.app.event_loop` when this method + is called. """ from pyglet import app if not self._context: @@ -676,7 +680,7 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): and advanced applications that must integrate their event loop into another framework. - Typical applications should use `pyglet.app.run`. + Typical applications should use :py:func:`pyglet.app.run`. """ raise NotImplementedError('abstract') @@ -715,11 +719,14 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): """Swap the OpenGL front and back buffers. Call this method on a double-buffered window to update the - visible display with the back buffer. The contents of the back buffer - is undefined after this operation. + visible display with the back buffer. Windows are + double-buffered by default unless you turn this feature off. - Windows are double-buffered by default. This method is called - automatically by `EventLoop` after the :py:meth:`~pyglet.window.Window.on_draw` event. + The contents of the back buffer are undefined after this operation. + + The default :py:attr:`~pyglet.app.event_loop` automatically + calls this method after the window's + :py:meth:`~pyglet.window.Window.on_draw` event. """ raise NotImplementedError('abstract') @@ -791,6 +798,25 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): """ raise NotImplementedError() + def get_clipboard_text(self) -> str: + """Access the system clipboard and attempt to retrieve text. + + :rtype: `str` + :return: A string from the clipboard. String will be empty if no text found. + """ + raise NotImplementedError() + + def set_clipboard_text(self, text: str): + """Access the system clipboard and set a text string as the clipboard data. + + This will clear the existing clipboard. + + :Parameters: + `text` : str + Text you want to place in the clipboard. + """ + raise NotImplementedError() + def minimize(self): """Minimize the window. """ @@ -1158,10 +1184,13 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass): def switch_to(self): """Make this window the current OpenGL rendering context. - Only one OpenGL context can be active at a time. This method sets - the current window's context to be current. You should use this - method in preference to `pyglet.gl.Context.set_current`, as it may - perform additional initialisation functions. + Only one OpenGL context can be active at a time. This method + sets the current window context as the active one. + + In most cases, you should use this method instead of directly + calling :py:meth:`pyglet.gl.Context.set_current`. The latter + will not perform platform-specific state management tasks for + you. """ raise NotImplementedError('abstract') diff --git a/libs/pyglet/window/cocoa/__init__.py b/libs/pyglet/window/cocoa/__init__.py index 6b127ec..89e627d 100644 --- a/libs/pyglet/window/cocoa/__init__.py +++ b/libs/pyglet/window/cocoa/__init__.py @@ -21,6 +21,7 @@ NSColor = cocoapy.ObjCClass('NSColor') NSEvent = cocoapy.ObjCClass('NSEvent') NSArray = cocoapy.ObjCClass('NSArray') NSImage = cocoapy.ObjCClass('NSImage') +NSPasteboard = cocoapy.ObjCClass('NSPasteboard') quartz = cocoapy.quartz cf = cocoapy.cf @@ -567,5 +568,32 @@ class CocoaWindow(BaseWindow): NSApp = NSApplication.sharedApplication() NSApp.setPresentationOptions_(options) + def set_clipboard_text(self, text: str): + with AutoReleasePool(): + pasteboard = NSPasteboard.generalPasteboard() + + pasteboard.clearContents() + + array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeString) + pasteboard.declareTypes_owner_(array, None) + + text_nsstring = cocoapy.get_NSString(text) + + pasteboard.setString_forType_(text_nsstring, cocoapy.NSPasteboardTypeString) + + def get_clipboard_text(self) -> str: + text = '' + with AutoReleasePool(): + pasteboard = NSPasteboard.generalPasteboard() + + if pasteboard.types().containsObject_(cocoapy.NSPasteboardTypeString): + text_obj = pasteboard.stringForType_(cocoapy.NSPasteboardTypeString) + if text_obj: + text = text_obj.UTF8String().decode('utf-8') + + return text + + + __all__ = ["CocoaWindow"] diff --git a/libs/pyglet/window/win32/__init__.py b/libs/pyglet/window/win32/__init__.py index bd37c86..2a58968 100644 --- a/libs/pyglet/window/win32/__init__.py +++ b/libs/pyglet/window/win32/__init__.py @@ -653,6 +653,43 @@ class Win32Window(BaseWindow): return icon + def set_clipboard_text(self, text: str): + valid = _user32.OpenClipboard(self._view_hwnd) + if not valid: + return + + _user32.EmptyClipboard() + + size = (len(text) + 1) * sizeof(WCHAR) # UTF-16 + + cb_data = _kernel32.GlobalAlloc(GMEM_MOVEABLE, size) + locked_data = _kernel32.GlobalLock(cb_data) + memmove(locked_data, text, size) # Trying to encode in utf-16 causes garbled text. Accepts str fine? + _kernel32.GlobalUnlock(cb_data) + + _user32.SetClipboardData(CF_UNICODETEXT, cb_data) + + _user32.CloseClipboard() + + def get_clipboard_text(self) -> str: + text = '' + + valid = _user32.OpenClipboard(self._view_hwnd) + if not valid: + print("Could not open clipboard") + return '' + + cb_obj = _user32.GetClipboardData(CF_UNICODETEXT) + if cb_obj: + locked_data = _kernel32.GlobalLock(cb_obj) + if locked_data: + text = ctypes.wstring_at(locked_data) + + _kernel32.GlobalUnlock(cb_obj) + + _user32.CloseClipboard() + return text + # Private util def _client_to_window_size(self, width, height): diff --git a/libs/pyglet/window/xlib/__init__.py b/libs/pyglet/window/xlib/__init__.py index cb42402..175b764 100644 --- a/libs/pyglet/window/xlib/__init__.py +++ b/libs/pyglet/window/xlib/__init__.py @@ -4,6 +4,7 @@ import urllib.parse from ctypes import * from functools import lru_cache +from typing import Optional import pyglet @@ -28,6 +29,7 @@ try: except ImportError: _have_xsync = False +_debug = pyglet.options['debug_x11'] class mwmhints_t(Structure): _fields_ = [ @@ -47,6 +49,7 @@ _can_detect_autorepeat = None XA_CARDINAL = 6 # Xatom.h:14 XA_ATOM = 4 +XA_STRING = 31 XDND_VERSION = 5 @@ -142,6 +145,9 @@ class XlibWindow(BaseWindow): if _can_detect_autorepeat: self.pressed_keys = set() + # Store clipboard string to not query as much for pasting a lot. + self._clipboard_str: Optional[str] = None + def _recreate(self, changes): # If flipping to/from fullscreen, need to recreate the window. (This # is the case with both override_redirect method and @@ -287,6 +293,12 @@ class XlibWindow(BaseWindow): # Atoms required for Xdnd self._create_xdnd_atoms(self._x_display) + # Clipboard related atoms + self._clipboard_atom = xlib.XInternAtom(self._x_display, asbytes('CLIPBOARD'), False) + self._utf8_atom = xlib.XInternAtom(self._x_display, asbytes('UTF8_STRING'), False) + self._target_atom = xlib.XInternAtom(self._x_display, asbytes('TARGETS'), False) + self._incr_atom = xlib.XInternAtom(self._x_display, asbytes('INCR'), False) + # Support for drag and dropping files needs to be enabled. if self._file_drops: # Some variables set because there are 4 different drop events that need shared data. @@ -798,6 +810,73 @@ class XlibWindow(BaseWindow): xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL, 32, xlib.PropModeReplace, buffer, len(data)//sizeof(c_ulong)) + def set_clipboard_text(self, text: str): + xlib.XSetSelectionOwner(self._x_display, + self._clipboard_atom, + self._window, + xlib.CurrentTime) + + if xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window: + self._clipboard_str = text + str_bytes = text.encode('utf-8') + size = len(str_bytes) + + xlib.XChangeProperty(self._x_display, self._window, + self._clipboard_atom, self._utf8_atom, 8, xlib.PropModeReplace, + (c_ubyte * size).from_buffer_copy(str_bytes), size) + else: + if _debug: + print("X11: Couldn't become owner of clipboard.") + + def get_clipboard_text(self) -> str: + if self._clipboard_str is not None: + return self._clipboard_str + + owner = xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) + + if not owner: + return '' + + text = '' + if owner == self._window: + data, size, actual_atom = self.get_single_property(self._window, self._clipboard_atom, + self._utf8_atom) + else: + notification = xlib.XEvent() + + # Convert to selection notification. + xlib.XConvertSelection(self._x_display, + self._clipboard_atom, + self._utf8_atom, + self._clipboard_atom, + self._window, + xlib.CurrentTime) + + while not xlib.XCheckTypedWindowEvent(self._x_display, self._window, xlib.SelectionNotify, byref(notification)): + self.dispatch_platform_event(notification) + + if not notification.xselection.property: + return '' + + data, size, actual_atom = self.get_single_property(notification.xselection.requestor, notification.xselection.property, + self._utf8_atom) + + if actual_atom == self._incr_atom: + # Not implemented. + if _debug: + print("X11: Clipboard data is too large, not implemented.") + + elif actual_atom == self._utf8_atom: + if data: + text_bytes = string_at(data, size) + + text = text_bytes.decode('utf-8') + + self._clipboard_str = text + + xlib.XFree(data) + return text + # Private utility def _set_wm_normal_hints(self): @@ -1328,7 +1407,7 @@ class XlibWindow(BaseWindow): xlib.XFree(data) def get_single_property(self, window, atom_property, atom_type): - """ Returns the length and data of a window property. """ + """ Returns the length, data, and actual atom of a window property. """ actualAtom = xlib.Atom() actualFormat = c_int() itemCount = c_ulong() @@ -1343,14 +1422,14 @@ class XlibWindow(BaseWindow): byref(bytesAfter), data) - return data, itemCount.value + return data, itemCount.value, actualAtom.value @XlibEventHandler(xlib.SelectionNotify) def _event_selection_notification(self, ev): if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']: if self._xdnd_format: # This will get the data - data, count = self.get_single_property(ev.xselection.requestor, + data, count, _ = self.get_single_property(ev.xselection.requestor, ev.xselection.property, ev.xselection.target) @@ -1514,5 +1593,61 @@ class XlibWindow(BaseWindow): self._mapped = False self.dispatch_event('on_hide') + @XlibEventHandler(xlib.SelectionClear) + def _event_selection_clear(self, ev): + if ev.xselectionclear.selection == self._clipboard_atom: + # Another application cleared the clipboard. + self._clipboard_str = None + + @XlibEventHandler(xlib.SelectionRequest) + def _event_selection_request(self, ev): + request = ev.xselectionrequest + + if _debug: + rt = xlib.XGetAtomName(self._x_display, request.target) + rp = xlib.XGetAtomName(self._x_display, request.property) + print(f"X11 debug: request target {rt}") + print(f"X11 debug: request property {rp}") + + out_event = xlib.XEvent() + out_event.xany.type = xlib.SelectionNotify + out_event.xselection.selection = request.selection + out_event.xselection.display = request.display + out_event.xselection.target = 0 + out_event.xselection.property = 0 + out_event.xselection.requestor = request.requestor + out_event.xselection.time = request.time + + if (xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window and + ev.xselection.target == self._clipboard_atom): + if request.target == self._target_atom: + atoms_ar = (xlib.Atom * 1)(self._utf8_atom) + ptr = cast(pointer(atoms_ar), POINTER(c_ubyte)) + + xlib.XChangeProperty(self._x_display, request.requestor, + request.property, XA_ATOM, 32, + xlib.PropModeReplace, + ptr, sizeof(atoms_ar)//sizeof(c_ulong)) + out_event.xselection.property = request.property + out_event.xselection.target = request.target + + elif request.target == self._utf8_atom: + # We are being requested for a UTF-8 string. + text = self._clipboard_str.encode('utf-8') + size = len(self._clipboard_str) + xlib.XChangeProperty(self._x_display, request.requestor, + request.property, request.target, 8, + xlib.PropModeReplace, + (c_ubyte * size).from_buffer_copy(text), size) + + out_event.xselection.property = request.property + out_event.xselection.target = request.target + + # Send request event back to requestor with updated changes. + xlib.XSendEvent(self._x_display, request.requestor, 0, 0, byref(out_event)) + + # Seems to work find without it. May add later. + #xlib.XSync(self._x_display, False) + __all__ = ["XlibEventHandler", "XlibWindow"] diff --git a/libs/utils/logger-old.py b/libs/utils/logger-old.py index 51b4813..86e74a9 100644 --- a/libs/utils/logger-old.py +++ b/libs/utils/logger-old.py @@ -1086,9 +1086,6 @@ def test_logger(the_logger: Logger): the_logger.debug('debugging') the_logger.info("Hello World!!") the_logger.info("Hello World!!") - the_logger.info("Hello World!!") - the_logger.info("Hello World!!") - the_logger.info("Hello World!!") the_logger.warn('warning') the_logger.warn('warning') the_logger.error('error haaaa') @@ -1111,9 +1108,9 @@ if __name__ == "__main__": a_logger.error('error haaaa') a_logger.fatal('oh no') logger.info('my name is:', logger.name) - for _ in range(5): - test_logger(logger) - test_logger(a_logger) + # for _ in range(5): + test_logger(logger) + test_logger(a_logger) print(Message_content(log_time=time.time(), text='aaa', level=4, marker='abc', end='abc', flush=False, frame=inspect.currentframe())) print(ColorCodeEnum.code_line.name) diff --git a/libs/utils/logger/__init__.py b/libs/utils/logger/__init__.py deleted file mode 100644 index 4b689da..0000000 --- a/libs/utils/logger/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- - - diff --git a/libs/utils/logger/logger.py b/libs/utils/logger/logger.py deleted file mode 100644 index b78e178..0000000 --- a/libs/utils/logger/logger.py +++ /dev/null @@ -1,9 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- - -from lib_not_dr.types.options import Options - - diff --git a/libs/utils/logger/types.py b/libs/utils/logger/types.py deleted file mode 100644 index 99bd6fb..0000000 --- a/libs/utils/logger/types.py +++ /dev/null @@ -1,31 +0,0 @@ -# ------------------------------- -# Difficult Rocket -# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com -# All rights reserved -# ------------------------------- - -import time - -from types import FrameType -from typing import List, Optional - -from lib_not_dr.types.options import Options - - -class LogMessage(Options): - name = 'LogMessage' - - # 消息内容本身的属性 - messages: List[str] = [] - end: str = '\n' - split: str = ' ' - - # 消息的属性 - flush: bool = True - level: int = 20 - log_time: float = time.time_ns() - logger_name: str = 'root' - logger_tag: Optional[str] = None - stack_trace: Optional[FrameType] = None - - diff --git a/mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock index c3f5d07..909375a 100644 --- a/mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock +++ b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ [[package]] name = "difficult_rocket_rs" -version = "0.2.22" +version = "0.2.23" dependencies = [ "pyo3", "quick-xml", @@ -415,9 +415,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", "serde", @@ -497,9 +497,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.186" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] @@ -518,9 +518,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.186" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -708,6 +708,6 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "xml-rs" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml index 233eb1d..c3d4aa1 100644 --- a/mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml +++ b/mods/dr_game/Difficult_Rocket_rs/src/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "difficult_rocket_rs" -version = "0.2.22" +version = "0.2.23" edition = "2021" license-file = '../../LICENSE' authors = [ @@ -22,15 +22,15 @@ opt-level = "s" # codegen-units = 1 [dependencies.quick-xml] -version = "0.30.0" +version = "0.31.0" features = ["serialize"] [dependencies.serde] -version = "1.0.186" +version = "1.0.190" features = ["derive"] [dependencies.xml-rs] -version = "0.8.16" +version = "0.8.19" [dependencies.serde-xml-rs] version = "0.6.0" diff --git a/mods/dr_game/__init__.py b/mods/dr_game/__init__.py index 2e1d463..9542e00 100644 --- a/mods/dr_game/__init__.py +++ b/mods/dr_game/__init__.py @@ -16,7 +16,7 @@ from Difficult_Rocket.api.mod import ModInfo from Difficult_Rocket.client import ClientWindow from Difficult_Rocket.api.types import Options, Version -DR_rust_version = Version("0.2.22.0") # DR_mod 的 Rust 编写部分的兼容版本 +DR_rust_version = Version("0.2.23.0") # DR_mod 的 Rust 编写部分的兼容版本 logger = logging.getLogger('client.dr_game') diff --git a/nuitka_build.py b/nuitka_build.py index c30defa..e5db8c5 100644 --- a/nuitka_build.py +++ b/nuitka_build.py @@ -47,14 +47,10 @@ if __name__ == '__main__': if '--report' in sys.argv: compiler.save_report = True sys.argv.remove('--report') - - # 检测 --output xx 参数 - if '--output' in sys.argv: - # 输入的是输出目录 - out_path = sys.argv[sys.argv.index('--output') + 1] - compiler.output_path = Path(out_path) - sys.argv.remove('--output') - sys.argv.remove(out_path) + + if '--lto=yes' in sys.argv: + compiler.use_lto = True + sys.argv.remove('--lto=yes') # 检测 --no-pyglet-opt 参数 pyglet_optimizations = True @@ -88,6 +84,16 @@ if __name__ == '__main__': pprint(compiler.option()) else: compiler.output_path = Path(f'./build/nuitka-{platform.system().lower()}') + compiler.show_memory = False + compiler.show_progress = False + + # 检测 --output xx 参数 + if '--output' in sys.argv: + # 输入的是输出目录 + out_path = sys.argv[sys.argv.index('--output') + 1] + compiler.output_path = Path(out_path) + sys.argv.remove('--output') + sys.argv.remove(out_path) print(compiler.as_markdown()) diff --git a/requirement-build.txt b/requirement-build.txt index da96f52..61451a4 100644 --- a/requirement-build.txt +++ b/requirement-build.txt @@ -4,23 +4,22 @@ # for images # not for pypy >= 3.10 -pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" +pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" # for sys info -psutil >= 5.9.5 +psutil >= 5.9.6 # for files rtoml >= 0.9.0 -tomlkit >= 0.11.8 +tomlkit >= 0.12.1 defusedxml >= 0.7.1 # for report error -objprint >= 0.2.2 +objprint >= 0.2.3 # for compile -nuitka >= 1.8.2 -ordered-set >= 4.1.0 -imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" -wheel >= 0.40.0 -setuptools >= 67.8.0 -setuptools-rust >= 1.6.0 +nuitka >= 1.8.5 +imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" +wheel >= 0.41.3 +setuptools >= 68.2.2 +setuptools-rust >= 1.8.1 \ No newline at end of file diff --git a/requirement-dev.txt b/requirement-dev.txt index eb58a12..e60107f 100644 --- a/requirement-dev.txt +++ b/requirement-dev.txt @@ -5,25 +5,24 @@ # for images # not for pypy >= 3.10 -pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" +pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" # for sys info -psutil >= 5.9.5 +psutil >= 5.9.6 # for files rtoml >= 0.9.0 -tomlkit >= 0.11.8 +tomlkit >= 0.12.1 defusedxml >= 0.7.1 # for debug -objprint >= 0.2.2 -viztracer >= 0.15.6; platform_python_implementation != "PyPy" +objprint >= 0.2.3 +viztracer >= 0.16.0; platform_python_implementation != "PyPy" vizplugins >= 0.1.3; platform_python_implementation != "PyPy" # for compile -nuitka >= 1.8.2 -ordered-set >= 4.1.0 -imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" -wheel >= 0.40.0 -setuptools >= 67.8.0 -setuptools-rust >= 1.6.0 +nuitka >= 1.8.5 +imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" +wheel >= 0.41.3 +setuptools >= 68.2.2 +setuptools-rust >= 1.8.1 diff --git a/requirement.txt b/requirement.txt index 316f5a8..6bdb503 100644 --- a/requirement.txt +++ b/requirement.txt @@ -3,15 +3,15 @@ # for images # not for pypy >= 3.10 -pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" +pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython" # for sys info -psutil >= 5.9.5 +psutil >= 5.9.6 # for files rtoml >= 0.9.0 -tomlkit >= 0.11.8 +tomlkit >= 0.12.1 defusedxml >= 0.7.1 # for report error -objprint >= 0.2.2 +objprint >= 0.2.3