mcpywrap 0.3.8__tar.gz → 0.3.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/CHANGELOG.md +5 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/MANIFEST.in +5 -0
- {mcpywrap-0.3.8/mcpywrap.egg-info → mcpywrap-0.3.9}/PKG-INFO +3 -1
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/README.md +2 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/releasing.md +3 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/__init__.py +1 -1
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/package_cmd.py +4 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/Injector.exe +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/Loader.dll +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/McpyMcsAuth.dll +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/manifest.json +3 -3
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/window.py +98 -17
- mcpywrap-0.3.9/mcpywrap/mcstudio/window_capture/manifest.json +4 -0
- mcpywrap-0.3.9/mcpywrap/mcstudio/window_capture/mcpy-window-capture.exe +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/client.py +14 -2
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/http_server.py +7 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9/mcpywrap.egg-info}/PKG-INFO +3 -1
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/SOURCES.txt +6 -0
- mcpywrap-0.3.9/native/window_capture/window_capture.cpp +282 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/pyproject.toml +2 -2
- mcpywrap-0.3.9/scripts/build_all_native.py +62 -0
- mcpywrap-0.3.9/scripts/build_window_capture.py +47 -0
- mcpywrap-0.3.9/scripts/check_window_capture.py +20 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/SKILL.md +1 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/troubleshooting.md +1 -1
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_game_window_skill.py +57 -1
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_package.py +18 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_remote.py +18 -2
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/LICENSE +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/code-libraries.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/development.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/engine-discovery.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/git-dependencies.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/local-dependencies.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/mcs-auth.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/qumod.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/__main__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/AddonsPack.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/MapPack.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/assembly.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/dependency_manager.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/file_merge.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/project_builder.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/watcher.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/cli.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/code_libraries.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/command_context.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/add_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/build_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/connect_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/default_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/dependency_prompt.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/desktop_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/dev_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/doctor_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/edit_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/init_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/mod_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/modsdk_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/publish_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/remove_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/run_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/serve_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/session_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/sync_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/ui_cmd.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/config.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/custom_packaging.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/dependencies.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/framework_presets.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/frameworks.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/git_cache.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/git_projects.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/SimpleMonitor.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/auth_interaction.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_assets.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/publisher.cer +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/diagnostics.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/discovery.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/editor.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/file_logs.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/game.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/log_protocol.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/mcs.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/mcs_auth.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/network.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/private_logs.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/processes.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/runtime_cppconfig.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/session_worker.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/sessions.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/studio_server.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/studio_server_ui.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlink_helper_global.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlink_helper_map.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlinks.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/addons.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/level_dat.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/map.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/netease_modsdk.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/template/generate_mod_files.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/template/mod_template.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/project_dependencies.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/project_init.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/service.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/source_files.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/ui/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/ui/project_ui.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/__init__.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/pip_error_parser.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/print_guide.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/project_setup.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/utils.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/dependency_links.txt +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/entry_points.txt +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/requires.txt +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/top_level.txt +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Bridge.cs +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Injector.cs +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Loader.c +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/publisher.cer +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/publisher.json +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/scripts/build_mcs_auth_bridge.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/scripts/check_mcs_auth_bridge.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/setup.cfg +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/agents/openai.yaml +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/project-dependencies.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/remote-testing.md +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/bootstrap.ps1 +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/bootstrap.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/game_window.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/smoke.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/manual_dependency_boundaries.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/manual_integration.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_auth_interaction.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_automation.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_code_libraries.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_dependency_boundaries.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_engine_discovery.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_git_projects.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_launch_encoding.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_local_auth_session.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_local_dependencies.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_mcs_auth.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_mouse.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_network.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_network_sessions.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_pack_uuid_conflicts.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_qumod_workflow.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_remote_scripts.py +0 -0
- {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_skill_workflows.py +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 0.3.9
|
|
4
|
+
|
|
5
|
+
- 截图默认使用 Windows Graphics Capture 捕获游戏客户区;被其他窗口遮挡时仍可读取画面,且不抢占前台。后台捕获不可用时自动回退到原有前台截图,并返回捕获方式和回退原因。
|
|
6
|
+
- 键盘和鼠标操作继续在执行前激活游戏窗口,操作后不恢复此前的前台窗口。新增 x64 捕获辅助程序及完整性校验;已在 Minecraft 3.10 实机验证 DirectX 画面、遮挡、DPI 尺寸和 F3 输入。
|
|
7
|
+
|
|
3
8
|
## 0.3.8
|
|
4
9
|
|
|
5
10
|
- 新增标准 Git 项目依赖:固定提交、导出结构识别、递归子依赖、循环/冲突检测、内容锁定,以及更新、回退和移除;克隆后通过 `mcpy sync` 恢复。
|
|
@@ -6,9 +6,14 @@ include docs/code-libraries.md docs/git-dependencies.md docs/qumod.md
|
|
|
6
6
|
recursive-include tests *.py
|
|
7
7
|
recursive-include skills *.md *.yaml *.ps1 *.py
|
|
8
8
|
include scripts/build_mcs_auth_bridge.py
|
|
9
|
+
include scripts/build_all_native.py
|
|
10
|
+
include scripts/build_window_capture.py
|
|
11
|
+
include scripts/check_window_capture.py
|
|
9
12
|
include scripts/check_mcs_auth_bridge.py
|
|
10
13
|
recursive-include native/mcs_auth *.cs *.c *.cer *.json
|
|
11
14
|
recursive-include mcpywrap/mcstudio/bridge_payload *.exe *.dll *.cer *.json
|
|
15
|
+
recursive-include mcpywrap/mcstudio/window_capture *.exe *.json
|
|
16
|
+
recursive-include native/window_capture *.cpp
|
|
12
17
|
prune test
|
|
13
18
|
prune .git
|
|
14
19
|
global-exclude __pycache__ *.py[cod] *.cppconfig *.log
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpywrap
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: A wrapper for Minecraft China Edition Addons management
|
|
5
5
|
Author-email: boybook <boybook@easecation.net>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,6 +46,8 @@ uv tool install mcpywrap
|
|
|
46
46
|
|
|
47
47
|
也可以使用 `pip install mcpywrap`。安装完成后,运行 `mcpy --help` 查看命令。
|
|
48
48
|
|
|
49
|
+
从 Windows 源码检出安装时,先安装 Visual Studio 的 x86/x64 C++ 构建工具,再运行 `python scripts/build_all_native.py` 一次编译全部原生组件,最后执行 `pip install -e .`。正式发布包由 CI 编译并附带原生组件,普通安装无需本机编译。
|
|
50
|
+
|
|
49
51
|
## 开始使用
|
|
50
52
|
|
|
51
53
|
进入你的 Addon 或地图项目目录,按向导初始化:
|
|
@@ -19,6 +19,8 @@ uv tool install mcpywrap
|
|
|
19
19
|
|
|
20
20
|
也可以使用 `pip install mcpywrap`。安装完成后,运行 `mcpy --help` 查看命令。
|
|
21
21
|
|
|
22
|
+
从 Windows 源码检出安装时,先安装 Visual Studio 的 x86/x64 C++ 构建工具,再运行 `python scripts/build_all_native.py` 一次编译全部原生组件,最后执行 `pip install -e .`。正式发布包由 CI 编译并附带原生组件,普通安装无需本机编译。
|
|
23
|
+
|
|
22
24
|
## 开始使用
|
|
23
25
|
|
|
24
26
|
进入你的 Addon 或地图项目目录,按向导初始化:
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
主分支和 PR 运行 Windows Python 3.9、3.12、3.14 的自动化测试,以及 macOS 的无 Qt 安装、项目构建与远程客户端测试。通过后构建 wheel/sdist、运行严格元数据检查、检查产物内容,并在独立环境安装 wheel 验证 CLI 和模块导入。
|
|
4
4
|
|
|
5
5
|
Windows CI 先用 MSVC 从 `native/mcs_auth` 编译 x86 登录桥接,再签名并通过构建产物交给测试和 Python 打包任务;编译后的 EXE/DLL 不提交到 Git。仓库只保留发布者公钥证书和指纹。
|
|
6
|
+
同一 native job 还会从 `native/window_capture` 编译 x64 后台截图辅助程序,生成 SHA-256 清单;测试和打包阶段校验二进制、源码及 wheel/sdist 内容。辅助程序不可用时运行时回退到前台截图。
|
|
7
|
+
|
|
8
|
+
从源码安装时,在 Windows x64 且已安装 Visual Studio x86/x64 C++ 工具的环境中先运行 `python scripts/build_all_native.py`,再执行 `pip install -e .` 或 `pip install .`。脚本一次编译 x86 登录桥接和 x64 截图程序到包目录;可用 `--output-root <目录>` 将结果放入隔离目录。登录桥接源码编译产物没有发布签名,正式登录能力仍应使用带签名的发布包。
|
|
6
9
|
|
|
7
10
|
仓库 Actions secrets:`MCS_BRIDGE_SIGNING_PFX` 保存 Base64 编码的加密 PFX,`MCS_BRIDGE_SIGNING_PASSWORD` 保存独立的强密码。只有 main/发布标签的签名步骤可读取它们,PR 只构建未签名测试产物。CI 将证书导入临时 runner 的个人证书库,签名后清理,不安装根信任。私钥、PFX 和密码均不进入日志或 artifact。
|
|
8
11
|
|
|
@@ -52,6 +52,10 @@ def package_cmd(merge):
|
|
|
52
52
|
# 新构建器保留源包目录名;分发包仍遵循 PR 的 name_bp/name_rp 约定。
|
|
53
53
|
for kind, suffix in (('behavior', 'bp'), ('resource', 'rp')):
|
|
54
54
|
folder = getattr(builder.target_addon, kind + '_pack_dir')
|
|
55
|
+
# 网易上传错误码24检查行为包entities目录;纯脚本Addon也保留
|
|
56
|
+
# 空目录条目,不创建虚构实体,不修改项目源码或外层包名。
|
|
57
|
+
if kind == 'behavior' and folder and Path(folder).is_dir():
|
|
58
|
+
(Path(folder) / 'entities').mkdir(exist_ok=True)
|
|
55
59
|
_zip_dir_keep_empty(zipf, folder, f'{name}_{suffix}')
|
|
56
60
|
else:
|
|
57
61
|
_zip_dir_keep_empty(zipf, output, '')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"Loader.c": "f2f1f781ce82fc30c62a6ce22bb048d3b023f6ff500e6ec8228da28b4dbf1b4a"
|
|
12
12
|
},
|
|
13
13
|
"files": {
|
|
14
|
-
"Injector.exe": "
|
|
15
|
-
"Loader.dll": "
|
|
16
|
-
"McpyMcsAuth.dll": "
|
|
14
|
+
"Injector.exe": "c2419774ecad30ad49e705d584223343bd1b26633879750d583a05c8fd563acf",
|
|
15
|
+
"Loader.dll": "8a3e2b44747d71ed05bf30bc0d752324c8b9e296653369a7fd9bdf4e0a2effce",
|
|
16
|
+
"McpyMcsAuth.dll": "c7d8caf91c26aedf00ae3fe4713d040da8c74393e53f17f57e3fe9960bce7a2c",
|
|
17
17
|
"publisher.cer": "e5aa2ac39df40804ed8b08fe8e43c4a2b8923f0d02a360708486b2db5b6303d7"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Windows 游戏会话的键盘输入与客户区截图,仅依赖 Python 标准库。"""
|
|
2
2
|
import ctypes as C
|
|
3
3
|
from ctypes import wintypes as W
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
4
6
|
import os
|
|
5
7
|
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
6
9
|
import struct
|
|
7
10
|
import time
|
|
8
11
|
import zlib
|
|
@@ -169,6 +172,10 @@ class BitmapHeader(C.Structure):
|
|
|
169
172
|
('used', W.DWORD), ('important', W.DWORD)]
|
|
170
173
|
|
|
171
174
|
|
|
175
|
+
class BackgroundCaptureUnavailable(ValueError):
|
|
176
|
+
"""The optional background capture path cannot produce a frame."""
|
|
177
|
+
|
|
178
|
+
|
|
172
179
|
class GameWindow:
|
|
173
180
|
def __init__(self, game, cancel=None):
|
|
174
181
|
if os.name != 'nt':
|
|
@@ -331,30 +338,37 @@ class GameWindow:
|
|
|
331
338
|
'sent': True, 'released': True, 'effect_verified': False}
|
|
332
339
|
|
|
333
340
|
def client_area(self):
|
|
341
|
+
return self._client_area(require_foreground=True, require_uncovered=True)
|
|
342
|
+
|
|
343
|
+
def _client_area(self, *, require_foreground, require_uncovered):
|
|
334
344
|
rect, point = W.RECT(), W.POINT()
|
|
335
345
|
if not self.user.GetClientRect(self.hwnd, C.byref(rect)) or not self.user.ClientToScreen(self.hwnd, C.byref(point)):
|
|
336
346
|
raise ValueError('无法获取游戏客户区')
|
|
337
347
|
width, height = rect.right, rect.bottom
|
|
338
348
|
if not 1 <= width * height <= 32_000_000:
|
|
339
349
|
raise ValueError('游戏窗口尺寸无效或超过 3200 万像素')
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if self.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
350
|
+
if require_foreground or require_uncovered:
|
|
351
|
+
left, top = self.user.GetSystemMetrics(76), self.user.GetSystemMetrics(77)
|
|
352
|
+
if not (left <= point.x and top <= point.y and point.x + width <= left + self.user.GetSystemMetrics(78)
|
|
353
|
+
and point.y + height <= top + self.user.GetSystemMetrics(79)):
|
|
354
|
+
raise ValueError('游戏客户区部分位于屏幕外,无法可靠截图')
|
|
355
|
+
if require_uncovered:
|
|
356
|
+
# 可见截图要求无上层窗口覆盖;宁可拒绝,也不截取其他应用内容。
|
|
357
|
+
for hwnd in self.windows():
|
|
358
|
+
if hwnd == self.hwnd:
|
|
359
|
+
break
|
|
360
|
+
if self.usable(hwnd) and not self.user.IsIconic(hwnd):
|
|
361
|
+
other = W.RECT()
|
|
362
|
+
if self.user.GetWindowRect(hwnd, C.byref(other)) and (
|
|
363
|
+
max(other.left, point.x) < min(other.right, point.x + width) and
|
|
364
|
+
max(other.top, point.y) < min(other.bottom, point.y + height)):
|
|
365
|
+
raise ValueError('游戏客户区被其他窗口覆盖,请移开后重试')
|
|
366
|
+
if require_foreground:
|
|
367
|
+
self.check_foreground()
|
|
355
368
|
return width, height, point
|
|
356
369
|
|
|
357
370
|
def screenshot(self):
|
|
371
|
+
"""Capture the visible client area after making the game foreground."""
|
|
358
372
|
self.foreground()
|
|
359
373
|
width, height, point = self.client_area()
|
|
360
374
|
screen = self.user.GetDC(None)
|
|
@@ -393,6 +407,73 @@ class GameWindow:
|
|
|
393
407
|
if screen:
|
|
394
408
|
self.user.ReleaseDC(None, screen)
|
|
395
409
|
|
|
410
|
+
def _background_helper(self):
|
|
411
|
+
if os.name != 'nt':
|
|
412
|
+
raise BackgroundCaptureUnavailable('Windows Graphics Capture 仅支持 Windows')
|
|
413
|
+
helper = Path(__file__).with_name('window_capture') / 'mcpy-window-capture.exe'
|
|
414
|
+
if not helper.is_file():
|
|
415
|
+
raise BackgroundCaptureUnavailable('未安装 Windows Graphics Capture 辅助程序')
|
|
416
|
+
try:
|
|
417
|
+
manifest = json.loads(helper.with_name('manifest.json').read_text(encoding='utf-8'))
|
|
418
|
+
if hashlib.sha256(helper.read_bytes()).hexdigest() != manifest['executable_sha256']:
|
|
419
|
+
raise BackgroundCaptureUnavailable('Windows Graphics Capture 辅助程序完整性校验失败')
|
|
420
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
421
|
+
raise BackgroundCaptureUnavailable(f'Windows Graphics Capture 辅助程序校验失败: {exc}') from exc
|
|
422
|
+
return helper
|
|
423
|
+
|
|
424
|
+
def screenshot_background(self):
|
|
425
|
+
"""Capture an occluded window without changing the foreground window."""
|
|
426
|
+
self.validate_process()
|
|
427
|
+
if self.owner(self.hwnd) != self.game['pid']:
|
|
428
|
+
raise ValueError('游戏窗口归属已改变')
|
|
429
|
+
if not self.usable(self.hwnd) or self.user.IsIconic(self.hwnd):
|
|
430
|
+
raise BackgroundCaptureUnavailable('游戏窗口不可进行后台捕获')
|
|
431
|
+
width, height, _ = self._client_area(require_foreground=False, require_uncovered=False)
|
|
432
|
+
helper = self._background_helper()
|
|
433
|
+
flags = subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
434
|
+
try:
|
|
435
|
+
completed = subprocess.run(
|
|
436
|
+
[str(helper), '--hwnd', str(int(self.hwnd))],
|
|
437
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
438
|
+
timeout=5, creationflags=flags, check=False)
|
|
439
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
440
|
+
raise BackgroundCaptureUnavailable(f'后台捕获程序不可用: {exc}') from exc
|
|
441
|
+
if completed.returncode:
|
|
442
|
+
reason = completed.stderr.decode('utf-8', errors='replace').strip()
|
|
443
|
+
raise BackgroundCaptureUnavailable(reason or f'后台捕获程序退出码 {completed.returncode}')
|
|
444
|
+
header, separator, content = completed.stdout.partition(b'\n')
|
|
445
|
+
fields = header.decode('ascii', errors='replace').split()
|
|
446
|
+
if len(fields) != 4 or fields[0] != 'MCPYCAP1':
|
|
447
|
+
raise BackgroundCaptureUnavailable('后台捕获返回格式无效')
|
|
448
|
+
try:
|
|
449
|
+
captured_width, captured_height, content_size = map(int, fields[1:])
|
|
450
|
+
except ValueError as exc:
|
|
451
|
+
raise BackgroundCaptureUnavailable('后台捕获尺寸无效') from exc
|
|
452
|
+
if not separator or content_size != len(content) or content[:8] != b'\x89PNG\r\n\x1a\n':
|
|
453
|
+
raise BackgroundCaptureUnavailable('后台捕获没有返回有效 PNG')
|
|
454
|
+
if (captured_width, captured_height) != (width, height):
|
|
455
|
+
raise BackgroundCaptureUnavailable(
|
|
456
|
+
f'后台捕获尺寸 {captured_width}x{captured_height} 与客户区 {width}x{height} 不一致')
|
|
457
|
+
self.validate_process()
|
|
458
|
+
return content, captured_width, captured_height
|
|
459
|
+
|
|
460
|
+
def screenshot_with_fallback(self):
|
|
461
|
+
try:
|
|
462
|
+
content, width, height = self.screenshot_background()
|
|
463
|
+
return content, width, height, {
|
|
464
|
+
'capture': 'background-window', 'capture_fallback': False,
|
|
465
|
+
}
|
|
466
|
+
except BackgroundCaptureUnavailable as background_error:
|
|
467
|
+
try:
|
|
468
|
+
content, width, height = self.screenshot()
|
|
469
|
+
except Exception as visible_error:
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f'后台截图不可用: {background_error}; 前台回退失败: {visible_error}') from visible_error
|
|
472
|
+
return content, width, height, {
|
|
473
|
+
'capture': 'visible-client-area', 'capture_fallback': True,
|
|
474
|
+
'capture_fallback_reason': str(background_error),
|
|
475
|
+
}
|
|
476
|
+
|
|
396
477
|
def mouse(self, action, *, x=None, y=None, width=None, height=None, to_x=None, to_y=None,
|
|
397
478
|
dx=0, dy=0, delta=0, button='left', duration_ms=80, keys=()):
|
|
398
479
|
if action not in ('move', 'click', 'double-click', 'scroll', 'drag', 'relative'):
|
|
@@ -520,8 +601,8 @@ def operate(project, session, action, parameters=None, cancel=None):
|
|
|
520
601
|
window = GameWindow(session_game(project, session), cancel=cancel)
|
|
521
602
|
try:
|
|
522
603
|
if action == 'screenshot':
|
|
523
|
-
content, width, height = window.
|
|
524
|
-
result = {'content': content, 'width': width, 'height': height,
|
|
604
|
+
content, width, height, capture = window.screenshot_with_fallback()
|
|
605
|
+
result = {'content': content, 'width': width, 'height': height, **capture}
|
|
525
606
|
elif action == 'key':
|
|
526
607
|
result = window.press(parameters['keys'], parameters.get('hold_ms', 80))
|
|
527
608
|
elif action == 'mouse':
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""CLI routing and standard-library transport. Never falls back or retries mutations."""
|
|
2
|
+
import base64
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
4
5
|
from http.client import HTTPException
|
|
@@ -66,8 +67,19 @@ class Client:
|
|
|
66
67
|
if not raw.startswith(b'\x89PNG\r\n\x1a\n') or len(raw) < 24:
|
|
67
68
|
raise RemoteError('远端未返回有效 PNG', 'invalid_response')
|
|
68
69
|
width, height = struct.unpack('>II', raw[16:24])
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
result = {'content': raw, 'width': width, 'height': height,
|
|
71
|
+
'service_instance': response.headers.get('X-Mcpy-Service-Instance')}
|
|
72
|
+
capture = response.headers.get('X-Mcpy-Capture')
|
|
73
|
+
if capture:
|
|
74
|
+
result['capture'] = capture
|
|
75
|
+
result['capture_fallback'] = response.headers.get('X-Mcpy-Capture-Fallback') == 'true'
|
|
76
|
+
reason = response.headers.get('X-Mcpy-Capture-Fallback-Reason-B64')
|
|
77
|
+
if reason:
|
|
78
|
+
try:
|
|
79
|
+
result['capture_fallback_reason'] = base64.b64decode(reason, validate=True).decode('utf-8')
|
|
80
|
+
except (ValueError, UnicodeError):
|
|
81
|
+
raise RemoteError('远端截图状态无效', 'invalid_response') from None
|
|
82
|
+
return result
|
|
71
83
|
try:
|
|
72
84
|
result = json.loads(raw.decode('utf-8-sig'))
|
|
73
85
|
except (ValueError, UnicodeError):
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Versioned, bounded HTTP interface; no file server, arbitrary paths, or shell endpoint."""
|
|
2
|
+
import base64
|
|
2
3
|
import hmac
|
|
3
4
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
4
5
|
import json
|
|
@@ -34,6 +35,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
34
35
|
self.send_header('Content-Type', 'image/png')
|
|
35
36
|
for key in ('width', 'height', 'session', 'service_instance'):
|
|
36
37
|
self.send_header('X-Mcpy-' + key.replace('_', '-'), str(data[key]))
|
|
38
|
+
if 'capture' in data:
|
|
39
|
+
self.send_header('X-Mcpy-Capture', data['capture'])
|
|
40
|
+
self.send_header('X-Mcpy-Capture-Fallback', 'true' if data.get('capture_fallback') else 'false')
|
|
41
|
+
if data.get('capture_fallback_reason'):
|
|
42
|
+
reason = base64.b64encode(data['capture_fallback_reason'].encode('utf-8')).decode('ascii')
|
|
43
|
+
self.send_header('X-Mcpy-Capture-Fallback-Reason-B64', reason)
|
|
37
44
|
else:
|
|
38
45
|
content = json.dumps({'ok': status < 400, 'error': None, 'hint': None, **data},
|
|
39
46
|
ensure_ascii=True, allow_nan=False).encode('utf-8')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpywrap
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: A wrapper for Minecraft China Edition Addons management
|
|
5
5
|
Author-email: boybook <boybook@easecation.net>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,6 +46,8 @@ uv tool install mcpywrap
|
|
|
46
46
|
|
|
47
47
|
也可以使用 `pip install mcpywrap`。安装完成后,运行 `mcpy --help` 查看命令。
|
|
48
48
|
|
|
49
|
+
从 Windows 源码检出安装时,先安装 Visual Studio 的 x86/x64 C++ 构建工具,再运行 `python scripts/build_all_native.py` 一次编译全部原生组件,最后执行 `pip install -e .`。正式发布包由 CI 编译并附带原生组件,普通安装无需本机编译。
|
|
50
|
+
|
|
49
51
|
## 开始使用
|
|
50
52
|
|
|
51
53
|
进入你的 Addon 或地图项目目录,按向导初始化:
|
|
@@ -90,6 +90,8 @@ mcpywrap/mcstudio/bridge_payload/Loader.dll
|
|
|
90
90
|
mcpywrap/mcstudio/bridge_payload/McpyMcsAuth.dll
|
|
91
91
|
mcpywrap/mcstudio/bridge_payload/manifest.json
|
|
92
92
|
mcpywrap/mcstudio/bridge_payload/publisher.cer
|
|
93
|
+
mcpywrap/mcstudio/window_capture/manifest.json
|
|
94
|
+
mcpywrap/mcstudio/window_capture/mcpy-window-capture.exe
|
|
93
95
|
mcpywrap/minecraft/__init__.py
|
|
94
96
|
mcpywrap/minecraft/addons.py
|
|
95
97
|
mcpywrap/minecraft/level_dat.py
|
|
@@ -113,8 +115,12 @@ native/mcs_auth/Injector.cs
|
|
|
113
115
|
native/mcs_auth/Loader.c
|
|
114
116
|
native/mcs_auth/publisher.cer
|
|
115
117
|
native/mcs_auth/publisher.json
|
|
118
|
+
native/window_capture/window_capture.cpp
|
|
119
|
+
scripts/build_all_native.py
|
|
116
120
|
scripts/build_mcs_auth_bridge.py
|
|
121
|
+
scripts/build_window_capture.py
|
|
117
122
|
scripts/check_mcs_auth_bridge.py
|
|
123
|
+
scripts/check_window_capture.py
|
|
118
124
|
skills/mcpywrap/SKILL.md
|
|
119
125
|
skills/mcpywrap/agents/openai.yaml
|
|
120
126
|
skills/mcpywrap/references/project-dependencies.md
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#define WIN32_LEAN_AND_MEAN
|
|
2
|
+
#define NOMINMAX
|
|
3
|
+
|
|
4
|
+
#include <windows.h>
|
|
5
|
+
#include <d3d11.h>
|
|
6
|
+
#include <dxgi1_2.h>
|
|
7
|
+
#include <dwmapi.h>
|
|
8
|
+
#include <fcntl.h>
|
|
9
|
+
#include <objidl.h>
|
|
10
|
+
#include <io.h>
|
|
11
|
+
#include <wincodec.h>
|
|
12
|
+
#include <wrl/client.h>
|
|
13
|
+
|
|
14
|
+
#include <winrt/base.h>
|
|
15
|
+
#include <winrt/Windows.Graphics.Capture.h>
|
|
16
|
+
#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
|
|
17
|
+
#include <windows.graphics.capture.interop.h>
|
|
18
|
+
#include <windows.graphics.directx.direct3d11.interop.h>
|
|
19
|
+
|
|
20
|
+
#include <algorithm>
|
|
21
|
+
#include <chrono>
|
|
22
|
+
#include <cstdint>
|
|
23
|
+
#include <iostream>
|
|
24
|
+
#include <limits>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <stdexcept>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
#pragma comment(lib, "d3d11.lib")
|
|
30
|
+
#pragma comment(lib, "dxgi.lib")
|
|
31
|
+
#pragma comment(lib, "dwmapi.lib")
|
|
32
|
+
#pragma comment(lib, "ole32.lib")
|
|
33
|
+
#pragma comment(lib, "windowscodecs.lib")
|
|
34
|
+
#pragma comment(lib, "windowsapp.lib")
|
|
35
|
+
|
|
36
|
+
using Microsoft::WRL::ComPtr;
|
|
37
|
+
using namespace winrt;
|
|
38
|
+
using namespace winrt::Windows::Graphics::Capture;
|
|
39
|
+
using namespace winrt::Windows::Graphics::DirectX;
|
|
40
|
+
using namespace winrt::Windows::Graphics::DirectX::Direct3D11;
|
|
41
|
+
|
|
42
|
+
namespace {
|
|
43
|
+
|
|
44
|
+
constexpr int kUnavailable = 2;
|
|
45
|
+
|
|
46
|
+
struct Pixels {
|
|
47
|
+
uint32_t width = 0;
|
|
48
|
+
uint32_t height = 0;
|
|
49
|
+
std::vector<uint8_t> bgra;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
[[noreturn]] void unavailable(const std::string& message) {
|
|
53
|
+
throw std::runtime_error(message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void check_bool(BOOL value, const char* message) {
|
|
57
|
+
if (!value) {
|
|
58
|
+
unavailable(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
HWND parse_hwnd(int argc, wchar_t** argv) {
|
|
63
|
+
for (int i = 1; i + 1 < argc; ++i) {
|
|
64
|
+
if (std::wstring(argv[i]) == L"--hwnd") {
|
|
65
|
+
try {
|
|
66
|
+
auto value = std::stoull(argv[i + 1]);
|
|
67
|
+
if (value == 0 || value > std::numeric_limits<uintptr_t>::max()) {
|
|
68
|
+
unavailable("无效的窗口句柄");
|
|
69
|
+
}
|
|
70
|
+
return reinterpret_cast<HWND>(static_cast<uintptr_t>(value));
|
|
71
|
+
} catch (...) {
|
|
72
|
+
unavailable("无效的窗口句柄");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
unavailable("缺少 --hwnd 参数");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
RECT client_bounds(HWND hwnd) {
|
|
80
|
+
RECT client{};
|
|
81
|
+
POINT origin{0, 0};
|
|
82
|
+
check_bool(GetClientRect(hwnd, &client), "无法获取游戏客户区");
|
|
83
|
+
check_bool(ClientToScreen(hwnd, &origin), "无法获取游戏客户区坐标");
|
|
84
|
+
const LONG width = client.right - client.left;
|
|
85
|
+
const LONG height = client.bottom - client.top;
|
|
86
|
+
client.left = origin.x;
|
|
87
|
+
client.top = origin.y;
|
|
88
|
+
client.right = origin.x + width;
|
|
89
|
+
client.bottom = origin.y + height;
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
struct UnmapGuard {
|
|
94
|
+
ID3D11DeviceContext* context;
|
|
95
|
+
ID3D11Resource* resource;
|
|
96
|
+
~UnmapGuard() { context->Unmap(resource, 0); }
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
Pixels read_frame(HWND hwnd, const Direct3D11CaptureFrame& frame,
|
|
100
|
+
const ComPtr<ID3D11Device>& device,
|
|
101
|
+
const ComPtr<ID3D11DeviceContext>& context) {
|
|
102
|
+
auto surface = frame.Surface();
|
|
103
|
+
auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
|
|
104
|
+
ComPtr<ID3D11Texture2D> source;
|
|
105
|
+
check_hresult(access->GetInterface(IID_PPV_ARGS(&source)));
|
|
106
|
+
|
|
107
|
+
D3D11_TEXTURE2D_DESC source_desc{};
|
|
108
|
+
source->GetDesc(&source_desc);
|
|
109
|
+
if (source_desc.Width == 0 || source_desc.Height == 0 || source_desc.Width > 32000 || source_desc.Height > 32000) {
|
|
110
|
+
unavailable("后台捕获尺寸无效");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
D3D11_TEXTURE2D_DESC staging_desc = source_desc;
|
|
114
|
+
staging_desc.Usage = D3D11_USAGE_STAGING;
|
|
115
|
+
staging_desc.BindFlags = 0;
|
|
116
|
+
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
|
117
|
+
staging_desc.MiscFlags = 0;
|
|
118
|
+
ComPtr<ID3D11Texture2D> staging;
|
|
119
|
+
check_hresult(device->CreateTexture2D(&staging_desc, nullptr, &staging));
|
|
120
|
+
context->CopyResource(staging.Get(), source.Get());
|
|
121
|
+
|
|
122
|
+
D3D11_MAPPED_SUBRESOURCE mapped{};
|
|
123
|
+
check_hresult(context->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &mapped));
|
|
124
|
+
UnmapGuard unmap{context.Get(), staging.Get()};
|
|
125
|
+
|
|
126
|
+
RECT client = client_bounds(hwnd);
|
|
127
|
+
RECT window{};
|
|
128
|
+
check_bool(GetWindowRect(hwnd, &window), "无法获取游戏窗口范围");
|
|
129
|
+
RECT extended = window;
|
|
130
|
+
DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &extended, sizeof(extended));
|
|
131
|
+
const LONG crop_width = client.right - client.left;
|
|
132
|
+
const LONG crop_height = client.bottom - client.top;
|
|
133
|
+
LONG crop_left = -1;
|
|
134
|
+
LONG crop_top = -1;
|
|
135
|
+
for (const RECT& bounds : {window, extended}) {
|
|
136
|
+
const LONG candidate_left = client.left - bounds.left;
|
|
137
|
+
const LONG candidate_top = client.top - bounds.top;
|
|
138
|
+
if (candidate_left >= 0 && candidate_top >= 0 && crop_width > 0 && crop_height > 0 &&
|
|
139
|
+
candidate_left + crop_width <= static_cast<LONG>(source_desc.Width) &&
|
|
140
|
+
candidate_top + crop_height <= static_cast<LONG>(source_desc.Height)) {
|
|
141
|
+
crop_left = candidate_left;
|
|
142
|
+
crop_top = candidate_top;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (crop_left < 0 || crop_top < 0) {
|
|
147
|
+
unavailable("后台捕获窗口范围与客户区不一致");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Pixels result;
|
|
151
|
+
result.width = static_cast<uint32_t>(crop_width);
|
|
152
|
+
result.height = static_cast<uint32_t>(crop_height);
|
|
153
|
+
result.bgra.resize(static_cast<size_t>(result.width) * result.height * 4);
|
|
154
|
+
const auto* base = static_cast<const uint8_t*>(mapped.pData);
|
|
155
|
+
for (uint32_t row = 0; row < result.height; ++row) {
|
|
156
|
+
const auto* source_row = base + static_cast<size_t>(crop_top + row) * mapped.RowPitch +
|
|
157
|
+
static_cast<size_t>(crop_left) * 4;
|
|
158
|
+
auto* target_row = result.bgra.data() + static_cast<size_t>(row) * result.width * 4;
|
|
159
|
+
std::copy_n(source_row, static_cast<size_t>(result.width) * 4, target_row);
|
|
160
|
+
}
|
|
161
|
+
bool has_color = false;
|
|
162
|
+
for (size_t offset = 0; offset < result.bgra.size(); offset += 4) {
|
|
163
|
+
if (result.bgra[offset] || result.bgra[offset + 1] || result.bgra[offset + 2]) {
|
|
164
|
+
has_color = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!has_color) {
|
|
169
|
+
unavailable("后台捕获返回全黑画面");
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
std::vector<uint8_t> encode_png(const Pixels& pixels) {
|
|
175
|
+
ComPtr<IWICImagingFactory> factory;
|
|
176
|
+
check_hresult(CoCreateInstance(CLSID_WICImagingFactory2, nullptr, CLSCTX_INPROC_SERVER,
|
|
177
|
+
IID_PPV_ARGS(&factory)));
|
|
178
|
+
ComPtr<IStream> stream;
|
|
179
|
+
check_hresult(CreateStreamOnHGlobal(nullptr, TRUE, &stream));
|
|
180
|
+
ComPtr<IWICBitmapEncoder> encoder;
|
|
181
|
+
check_hresult(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder));
|
|
182
|
+
check_hresult(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache));
|
|
183
|
+
ComPtr<IWICBitmapFrameEncode> frame;
|
|
184
|
+
ComPtr<IPropertyBag2> properties;
|
|
185
|
+
check_hresult(encoder->CreateNewFrame(&frame, &properties));
|
|
186
|
+
check_hresult(frame->Initialize(properties.Get()));
|
|
187
|
+
check_hresult(frame->SetSize(pixels.width, pixels.height));
|
|
188
|
+
WICPixelFormatGUID format = GUID_WICPixelFormat32bppBGRA;
|
|
189
|
+
check_hresult(frame->SetPixelFormat(&format));
|
|
190
|
+
check_hresult(frame->WritePixels(pixels.height, pixels.width * 4,
|
|
191
|
+
static_cast<UINT>(pixels.bgra.size()),
|
|
192
|
+
const_cast<BYTE*>(pixels.bgra.data())));
|
|
193
|
+
check_hresult(frame->Commit());
|
|
194
|
+
check_hresult(encoder->Commit());
|
|
195
|
+
|
|
196
|
+
HGLOBAL handle = nullptr;
|
|
197
|
+
check_hresult(GetHGlobalFromStream(stream.Get(), &handle));
|
|
198
|
+
STATSTG status{};
|
|
199
|
+
check_hresult(stream->Stat(&status, STATFLAG_NONAME));
|
|
200
|
+
const ULONGLONG stream_size = status.cbSize.QuadPart;
|
|
201
|
+
if (!stream_size || stream_size > std::numeric_limits<uint32_t>::max()) {
|
|
202
|
+
unavailable("PNG 输出尺寸无效");
|
|
203
|
+
}
|
|
204
|
+
const auto* data = static_cast<const uint8_t*>(GlobalLock(handle));
|
|
205
|
+
if (!data) {
|
|
206
|
+
unavailable("无法读取 PNG 输出");
|
|
207
|
+
}
|
|
208
|
+
std::vector<uint8_t> output(data, data + static_cast<size_t>(stream_size));
|
|
209
|
+
GlobalUnlock(handle);
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Pixels capture(HWND hwnd) {
|
|
214
|
+
if (!IsWindow(hwnd) || !IsWindowVisible(hwnd) || IsIconic(hwnd)) {
|
|
215
|
+
unavailable("游戏窗口不可进行后台捕获");
|
|
216
|
+
}
|
|
217
|
+
DWORD cloaked = 0;
|
|
218
|
+
if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked))) && cloaked) {
|
|
219
|
+
unavailable("游戏窗口被系统隐藏");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
ComPtr<ID3D11Device> device;
|
|
223
|
+
ComPtr<ID3D11DeviceContext> context;
|
|
224
|
+
D3D_FEATURE_LEVEL feature_level{};
|
|
225
|
+
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
|
|
226
|
+
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0};
|
|
227
|
+
check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
|
|
228
|
+
D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels,
|
|
229
|
+
ARRAYSIZE(levels), D3D11_SDK_VERSION, &device,
|
|
230
|
+
&feature_level, &context));
|
|
231
|
+
ComPtr<IDXGIDevice> dxgi_device;
|
|
232
|
+
check_hresult(device.As(&dxgi_device));
|
|
233
|
+
winrt::com_ptr<::IInspectable> inspectable;
|
|
234
|
+
check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()));
|
|
235
|
+
auto direct3d_device = inspectable.as<IDirect3DDevice>();
|
|
236
|
+
|
|
237
|
+
GraphicsCaptureItem item{nullptr};
|
|
238
|
+
auto interop = get_activation_factory<GraphicsCaptureItem, IGraphicsCaptureItemInterop>();
|
|
239
|
+
check_hresult(interop->CreateForWindow(hwnd, guid_of<GraphicsCaptureItem>(),
|
|
240
|
+
reinterpret_cast<void**>(put_abi(item))));
|
|
241
|
+
auto pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
|
|
242
|
+
direct3d_device, DirectXPixelFormat::B8G8R8A8UIntNormalized, 1, item.Size());
|
|
243
|
+
auto session = pool.CreateCaptureSession(item);
|
|
244
|
+
session.IsCursorCaptureEnabled(false);
|
|
245
|
+
session.StartCapture();
|
|
246
|
+
|
|
247
|
+
Direct3D11CaptureFrame frame{nullptr};
|
|
248
|
+
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
|
249
|
+
while (!frame && std::chrono::steady_clock::now() < deadline) {
|
|
250
|
+
frame = pool.TryGetNextFrame();
|
|
251
|
+
if (!frame) {
|
|
252
|
+
Sleep(20);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!frame) {
|
|
256
|
+
unavailable("后台捕获未返回画面");
|
|
257
|
+
}
|
|
258
|
+
return read_frame(hwnd, frame, device, context);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
} // namespace
|
|
262
|
+
|
|
263
|
+
int wmain(int argc, wchar_t** argv) {
|
|
264
|
+
try {
|
|
265
|
+
init_apartment(apartment_type::multi_threaded);
|
|
266
|
+
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
|
267
|
+
const HWND hwnd = parse_hwnd(argc, argv);
|
|
268
|
+
const Pixels pixels = capture(hwnd);
|
|
269
|
+
const auto png = encode_png(pixels);
|
|
270
|
+
_setmode(_fileno(stdout), _O_BINARY);
|
|
271
|
+
std::cout << "MCPYCAP1 " << pixels.width << ' ' << pixels.height << ' ' << png.size() << '\n';
|
|
272
|
+
std::cout.write(reinterpret_cast<const char*>(png.data()), static_cast<std::streamsize>(png.size()));
|
|
273
|
+
std::cout.flush();
|
|
274
|
+
return 0;
|
|
275
|
+
} catch (const winrt::hresult_error& error) {
|
|
276
|
+
std::wcerr << error.message().c_str();
|
|
277
|
+
return kUnavailable;
|
|
278
|
+
} catch (const std::exception& error) {
|
|
279
|
+
std::cerr << error.what();
|
|
280
|
+
return kUnavailable;
|
|
281
|
+
}
|
|
282
|
+
}
|