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.
Files changed (156) hide show
  1. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/CHANGELOG.md +5 -0
  2. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/MANIFEST.in +5 -0
  3. {mcpywrap-0.3.8/mcpywrap.egg-info → mcpywrap-0.3.9}/PKG-INFO +3 -1
  4. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/README.md +2 -0
  5. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/releasing.md +3 -0
  6. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/__init__.py +1 -1
  7. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/package_cmd.py +4 -0
  8. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/Injector.exe +0 -0
  9. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/Loader.dll +0 -0
  10. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/McpyMcsAuth.dll +0 -0
  11. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/manifest.json +3 -3
  12. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/window.py +98 -17
  13. mcpywrap-0.3.9/mcpywrap/mcstudio/window_capture/manifest.json +4 -0
  14. mcpywrap-0.3.9/mcpywrap/mcstudio/window_capture/mcpy-window-capture.exe +0 -0
  15. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/client.py +14 -2
  16. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/http_server.py +7 -0
  17. {mcpywrap-0.3.8 → mcpywrap-0.3.9/mcpywrap.egg-info}/PKG-INFO +3 -1
  18. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/SOURCES.txt +6 -0
  19. mcpywrap-0.3.9/native/window_capture/window_capture.cpp +282 -0
  20. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/pyproject.toml +2 -2
  21. mcpywrap-0.3.9/scripts/build_all_native.py +62 -0
  22. mcpywrap-0.3.9/scripts/build_window_capture.py +47 -0
  23. mcpywrap-0.3.9/scripts/check_window_capture.py +20 -0
  24. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/SKILL.md +1 -0
  25. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/troubleshooting.md +1 -1
  26. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_game_window_skill.py +57 -1
  27. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_package.py +18 -0
  28. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_remote.py +18 -2
  29. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/LICENSE +0 -0
  30. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/code-libraries.md +0 -0
  31. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/development.md +0 -0
  32. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/engine-discovery.md +0 -0
  33. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/git-dependencies.md +0 -0
  34. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/local-dependencies.md +0 -0
  35. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/mcs-auth.md +0 -0
  36. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/docs/qumod.md +0 -0
  37. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/__main__.py +0 -0
  38. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/AddonsPack.py +0 -0
  39. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/MapPack.py +0 -0
  40. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/__init__.py +0 -0
  41. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/assembly.py +0 -0
  42. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/dependency_manager.py +0 -0
  43. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/file_merge.py +0 -0
  44. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/project_builder.py +0 -0
  45. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/builders/watcher.py +0 -0
  46. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/cli.py +0 -0
  47. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/code_libraries.py +0 -0
  48. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/command_context.py +0 -0
  49. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/__init__.py +0 -0
  50. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/add_cmd.py +0 -0
  51. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/build_cmd.py +0 -0
  52. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/connect_cmd.py +0 -0
  53. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/default_cmd.py +0 -0
  54. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/dependency_prompt.py +0 -0
  55. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/desktop_cmd.py +0 -0
  56. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/dev_cmd.py +0 -0
  57. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/doctor_cmd.py +0 -0
  58. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/edit_cmd.py +0 -0
  59. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/init_cmd.py +0 -0
  60. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/mod_cmd.py +0 -0
  61. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/modsdk_cmd.py +0 -0
  62. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/publish_cmd.py +0 -0
  63. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/remove_cmd.py +0 -0
  64. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/run_cmd.py +0 -0
  65. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/serve_cmd.py +0 -0
  66. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/session_cmd.py +0 -0
  67. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/sync_cmd.py +0 -0
  68. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/commands/ui_cmd.py +0 -0
  69. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/config.py +0 -0
  70. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/custom_packaging.py +0 -0
  71. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/dependencies.py +0 -0
  72. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/framework_presets.py +0 -0
  73. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/frameworks.py +0 -0
  74. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/git_cache.py +0 -0
  75. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/git_projects.py +0 -0
  76. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/SimpleMonitor.py +0 -0
  77. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/__init__.py +0 -0
  78. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/auth_interaction.py +0 -0
  79. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_assets.py +0 -0
  80. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/bridge_payload/publisher.cer +0 -0
  81. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/diagnostics.py +0 -0
  82. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/discovery.py +0 -0
  83. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/editor.py +0 -0
  84. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/file_logs.py +0 -0
  85. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/game.py +0 -0
  86. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/log_protocol.py +0 -0
  87. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/mcs.py +0 -0
  88. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/mcs_auth.py +0 -0
  89. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/network.py +0 -0
  90. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/private_logs.py +0 -0
  91. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/processes.py +0 -0
  92. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/runtime_cppconfig.py +0 -0
  93. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/session_worker.py +0 -0
  94. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/sessions.py +0 -0
  95. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/studio_server.py +0 -0
  96. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/studio_server_ui.py +0 -0
  97. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlink_helper_global.py +0 -0
  98. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlink_helper_map.py +0 -0
  99. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/mcstudio/symlinks.py +0 -0
  100. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/__init__.py +0 -0
  101. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/addons.py +0 -0
  102. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/level_dat.py +0 -0
  103. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/map.py +0 -0
  104. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/netease_modsdk.py +0 -0
  105. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/template/generate_mod_files.py +0 -0
  106. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/minecraft/template/mod_template.py +0 -0
  107. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/project_dependencies.py +0 -0
  108. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/project_init.py +0 -0
  109. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/__init__.py +0 -0
  110. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/remote/service.py +0 -0
  111. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/source_files.py +0 -0
  112. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/ui/__init__.py +0 -0
  113. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/ui/project_ui.py +0 -0
  114. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/__init__.py +0 -0
  115. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/pip_error_parser.py +0 -0
  116. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/print_guide.py +0 -0
  117. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/project_setup.py +0 -0
  118. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap/utils/utils.py +0 -0
  119. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/dependency_links.txt +0 -0
  120. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/entry_points.txt +0 -0
  121. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/requires.txt +0 -0
  122. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/mcpywrap.egg-info/top_level.txt +0 -0
  123. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Bridge.cs +0 -0
  124. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Injector.cs +0 -0
  125. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/Loader.c +0 -0
  126. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/publisher.cer +0 -0
  127. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/native/mcs_auth/publisher.json +0 -0
  128. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/scripts/build_mcs_auth_bridge.py +0 -0
  129. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/scripts/check_mcs_auth_bridge.py +0 -0
  130. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/setup.cfg +0 -0
  131. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/agents/openai.yaml +0 -0
  132. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/project-dependencies.md +0 -0
  133. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/references/remote-testing.md +0 -0
  134. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/bootstrap.ps1 +0 -0
  135. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/bootstrap.py +0 -0
  136. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/game_window.py +0 -0
  137. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/skills/mcpywrap/scripts/smoke.py +0 -0
  138. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/manual_dependency_boundaries.py +0 -0
  139. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/manual_integration.py +0 -0
  140. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_auth_interaction.py +0 -0
  141. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_automation.py +0 -0
  142. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_code_libraries.py +0 -0
  143. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_dependency_boundaries.py +0 -0
  144. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_engine_discovery.py +0 -0
  145. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_git_projects.py +0 -0
  146. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_launch_encoding.py +0 -0
  147. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_local_auth_session.py +0 -0
  148. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_local_dependencies.py +0 -0
  149. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_mcs_auth.py +0 -0
  150. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_mouse.py +0 -0
  151. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_network.py +0 -0
  152. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_network_sessions.py +0 -0
  153. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_pack_uuid_conflicts.py +0 -0
  154. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_qumod_workflow.py +0 -0
  155. {mcpywrap-0.3.8 → mcpywrap-0.3.9}/tests/test_remote_scripts.py +0 -0
  156. {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.8
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
 
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """mcpywrap - 我的世界中国版 Addon 开发依赖、构建与游戏测试工具。"""
3
3
 
4
- __version__ = '0.3.8'
4
+ __version__ = '0.3.9'
@@ -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, '')
@@ -11,9 +11,9 @@
11
11
  "Loader.c": "f2f1f781ce82fc30c62a6ce22bb048d3b023f6ff500e6ec8228da28b4dbf1b4a"
12
12
  },
13
13
  "files": {
14
- "Injector.exe": "077d4d84bfa3d7f9fac2485bc7bc3def415596a94ef2fe0815580446ee4552b6",
15
- "Loader.dll": "ea081696b47cb16dfed8b61a95ffbba7d4a66de7bb1769f477cb688099186527",
16
- "McpyMcsAuth.dll": "7f0f72b3d38577823e7e280d6378692f9fdfe7f2c0546938aea03312c59431f3",
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
- left, top = self.user.GetSystemMetrics(76), self.user.GetSystemMetrics(77)
341
- if not (left <= point.x and top <= point.y and point.x + width <= left + self.user.GetSystemMetrics(78)
342
- and point.y + height <= top + self.user.GetSystemMetrics(79)):
343
- raise ValueError('游戏客户区部分位于屏幕外,无法可靠截图')
344
- # 可见截图要求无上层窗口覆盖;宁可拒绝,也不截取其他应用内容。
345
- for hwnd in self.windows():
346
- if hwnd == self.hwnd:
347
- break
348
- if self.usable(hwnd) and not self.user.IsIconic(hwnd):
349
- other = W.RECT()
350
- if self.user.GetWindowRect(hwnd, C.byref(other)) and (
351
- max(other.left, point.x) < min(other.right, point.x + width) and
352
- max(other.top, point.y) < min(other.bottom, point.y + height)):
353
- raise ValueError('游戏客户区被其他窗口覆盖,请移开后重试')
354
- self.check_foreground()
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.screenshot()
524
- result = {'content': content, 'width': width, 'height': height, 'capture': 'visible-client-area'}
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':
@@ -0,0 +1,4 @@
1
+ {
2
+ "executable_sha256": "baface6b59f67379eb74fcbfe7803d5a1dfee1ac73ff7dbbb44e38e5c7c11853",
3
+ "source_sha256": "8f00a92f7a4418bfe59b5d1aea0547f1cb06aac8163b698932cabd1b1f829883"
4
+ }
@@ -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
- return {'content': raw, 'width': width, 'height': height,
70
- 'service_instance': response.headers.get('X-Mcpy-Service-Instance')}
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.8
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
+ }