mcpywrap 0.3.2__tar.gz → 0.3.3__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 (97) hide show
  1. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/CHANGELOG.md +7 -0
  2. {mcpywrap-0.3.2/mcpywrap.egg-info → mcpywrap-0.3.3}/PKG-INFO +23 -3
  3. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/README.md +22 -2
  4. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/__init__.py +1 -1
  5. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/cli.py +2 -0
  6. mcpywrap-0.3.3/mcpywrap/commands/connect_cmd.py +18 -0
  7. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/run_cmd.py +23 -1
  8. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/ui_cmd.py +4 -0
  9. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/discovery.py +4 -4
  10. mcpywrap-0.3.3/mcpywrap/mcstudio/network.py +153 -0
  11. {mcpywrap-0.3.2 → mcpywrap-0.3.3/mcpywrap.egg-info}/PKG-INFO +23 -3
  12. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap.egg-info/SOURCES.txt +3 -0
  13. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/pyproject.toml +1 -1
  14. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/SKILL.md +20 -3
  15. mcpywrap-0.3.3/tests/test_network.py +228 -0
  16. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/LICENSE +0 -0
  17. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/MANIFEST.in +0 -0
  18. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/docs/development.md +0 -0
  19. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/docs/engine-discovery.md +0 -0
  20. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/docs/local-dependencies.md +0 -0
  21. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/docs/releasing.md +0 -0
  22. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/__main__.py +0 -0
  23. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/AddonsPack.py +0 -0
  24. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/MapPack.py +0 -0
  25. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/__init__.py +0 -0
  26. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/assembly.py +0 -0
  27. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/dependency_manager.py +0 -0
  28. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/file_merge.py +0 -0
  29. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/project_builder.py +0 -0
  30. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/builders/watcher.py +0 -0
  31. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/command_context.py +0 -0
  32. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/__init__.py +0 -0
  33. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/add_cmd.py +0 -0
  34. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/build_cmd.py +0 -0
  35. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/default_cmd.py +0 -0
  36. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/dependency_prompt.py +0 -0
  37. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/dev_cmd.py +0 -0
  38. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/doctor_cmd.py +0 -0
  39. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/edit_cmd.py +0 -0
  40. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/init_cmd.py +0 -0
  41. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/mod_cmd.py +0 -0
  42. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/modsdk_cmd.py +0 -0
  43. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/package_cmd.py +0 -0
  44. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/publish_cmd.py +0 -0
  45. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/remove_cmd.py +0 -0
  46. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/session_cmd.py +0 -0
  47. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/commands/sync_cmd.py +0 -0
  48. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/config.py +0 -0
  49. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/custom_packaging.py +0 -0
  50. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/dependencies.py +0 -0
  51. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/SimpleMonitor.py +0 -0
  52. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/__init__.py +0 -0
  53. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/editor.py +0 -0
  54. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/file_logs.py +0 -0
  55. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/game.py +0 -0
  56. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/log_protocol.py +0 -0
  57. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/mcs.py +0 -0
  58. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/processes.py +0 -0
  59. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/runtime_cppconfig.py +0 -0
  60. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/session_worker.py +0 -0
  61. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/sessions.py +0 -0
  62. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/studio_server.py +0 -0
  63. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/studio_server_ui.py +0 -0
  64. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/symlink_helper_global.py +0 -0
  65. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/symlink_helper_map.py +0 -0
  66. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/mcstudio/symlinks.py +0 -0
  67. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/__init__.py +0 -0
  68. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/addons.py +0 -0
  69. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/level_dat.py +0 -0
  70. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/map.py +0 -0
  71. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/netease_modsdk.py +0 -0
  72. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/template/generate_mod_files.py +0 -0
  73. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/minecraft/template/mod_template.py +0 -0
  74. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/project_init.py +0 -0
  75. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/ui/__init__.py +0 -0
  76. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/ui/project_ui.py +0 -0
  77. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/utils/__init__.py +0 -0
  78. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/utils/pip_error_parser.py +0 -0
  79. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/utils/print_guide.py +0 -0
  80. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/utils/project_setup.py +0 -0
  81. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap/utils/utils.py +0 -0
  82. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap.egg-info/dependency_links.txt +0 -0
  83. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap.egg-info/entry_points.txt +0 -0
  84. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap.egg-info/requires.txt +0 -0
  85. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/mcpywrap.egg-info/top_level.txt +0 -0
  86. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/setup.cfg +0 -0
  87. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/agents/openai.yaml +0 -0
  88. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/references/troubleshooting.md +0 -0
  89. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/scripts/bootstrap.ps1 +0 -0
  90. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/scripts/game_window.py +0 -0
  91. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/skills/mcpywrap/scripts/smoke.py +0 -0
  92. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/manual_integration.py +0 -0
  93. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/test_automation.py +0 -0
  94. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/test_engine_discovery.py +0 -0
  95. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/test_game_window_skill.py +0 -0
  96. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/test_local_dependencies.py +0 -0
  97. {mcpywrap-0.3.2 → mcpywrap-0.3.3}/tests/test_package.py +0 -0
@@ -1,5 +1,12 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.3.3
4
+
5
+ - 新增实验性 `mcpy connect HOST --port PORT`,无需项目配置或登录身份即可尝试连接服务器。
6
+ - `mcpy run` 支持 `[tool.mcpywrap.server]`,可用于纯连接目录或已有 Addon 项目;依赖继续校验,暂不装配网络服 Mod。
7
+ - 网络运行共用前台调度,采集 SDK 与引擎日志;Ctrl+C 结束本次进程并清理临时配置,本地世界流程保持兼容。
8
+ - README 与 Skill 补充精简用法及网络模式限制;126 项回归测试通过,用户已确认本机未认证连接测试通过,其他服务器是否允许连接取决于其认证要求。
9
+
3
10
  ## 0.3.2
4
11
 
5
12
  - 新增 `package`,构建项目及依赖并生成分发 ZIP,失败保留已有产物。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpywrap
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A wrapper for Minecraft China Edition Addons management
5
5
  Author-email: boybook <boybook@easecation.net>
6
6
  License-Expression: MIT
@@ -96,7 +96,7 @@ mcpy add --path "../shared-addon"
96
96
  | `mcpy run -l` | 查看已有实例 |
97
97
  | `mcpy run -d <ID前缀>` | 删除指定实例 |
98
98
 
99
- `mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
99
+ 本地世界模式下,`mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
100
100
 
101
101
  ### 打包分发
102
102
 
@@ -124,6 +124,26 @@ mcpy run --engine-version 3.10.0.420447
124
124
 
125
125
  已有实例默认保留原引擎版本;显式指定版本可以切换。自定义路径、项目级设置和环境变量的用法见[引擎配置参考](https://github.com/EaseCation/mcpywrap/blob/main/docs/engine-discovery.md)。
126
126
 
127
+ ### 连接服务器(实验性)
128
+
129
+ 临时连接无需项目配置,地址可以是 IP 或主机名,端口默认 `19132`:
130
+
131
+ ```powershell
132
+ mcpy connect 192.168.31.101 --port 19132
133
+ ```
134
+
135
+ 固定目标可写入 `pyproject.toml`,随后在该目录执行 `mcpy run`:
136
+
137
+ ```toml
138
+ [tool.mcpywrap.server]
139
+ host = "192.168.31.101"
140
+ port = 19132
141
+ ```
142
+
143
+ 空目录只需上述配置,无需执行 `mcpy init`;已有 Addon 项目也可添加此表。当前使用未认证连接,不读取登录身份或 token,不保证服务器允许进服;项目依赖仍会校验,但暂不装配本地 Mod。
144
+
145
+ 网络模式前台输出日志并显示日志文件路径,按 Ctrl+C 结束本次游戏。暂不支持 Map 项目、GUI、`--detach`、`--new` 或本地世界实例 ID。
146
+
127
147
  ## AI Agent 使用
128
148
 
129
149
  仓库提供标准 [mcpywrap Skill](https://github.com/EaseCation/mcpywrap/tree/main/skills/mcpywrap),帮助 Agent 安装工具、管理依赖、
@@ -136,7 +156,7 @@ mcpy run --engine-version 3.10.0.420447
136
156
  也可以下载仓库 ZIP,将完整的 `skills/mcpywrap` 文件夹复制到对应 Agent 的技能目录。
137
157
  无需从源码安装 Python 项目;CLI 和 Skill 分别安装,`pip/uv install` 不会自动注册 Skill。
138
158
  Skill 中的 `scripts/bootstrap.ps1` 可使用已有 uv 安装 CLI,并检查是否具备所需命令能力。
139
- 请使用 mcpywrap 0.3.2 或更高版本;需要固定组合时,可从 `v0.3.2` 标签安装对应 Skill。
159
+ 请使用 mcpywrap 0.3.3 或更高版本;需要固定组合时,可从 `v0.3.3` 标签安装对应 Skill。
140
160
 
141
161
  安装后可直接描述任务:
142
162
 
@@ -69,7 +69,7 @@ mcpy add --path "../shared-addon"
69
69
  | `mcpy run -l` | 查看已有实例 |
70
70
  | `mcpy run -d <ID前缀>` | 删除指定实例 |
71
71
 
72
- `mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
72
+ 本地世界模式下,`mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
73
73
 
74
74
  ### 打包分发
75
75
 
@@ -97,6 +97,26 @@ mcpy run --engine-version 3.10.0.420447
97
97
 
98
98
  已有实例默认保留原引擎版本;显式指定版本可以切换。自定义路径、项目级设置和环境变量的用法见[引擎配置参考](https://github.com/EaseCation/mcpywrap/blob/main/docs/engine-discovery.md)。
99
99
 
100
+ ### 连接服务器(实验性)
101
+
102
+ 临时连接无需项目配置,地址可以是 IP 或主机名,端口默认 `19132`:
103
+
104
+ ```powershell
105
+ mcpy connect 192.168.31.101 --port 19132
106
+ ```
107
+
108
+ 固定目标可写入 `pyproject.toml`,随后在该目录执行 `mcpy run`:
109
+
110
+ ```toml
111
+ [tool.mcpywrap.server]
112
+ host = "192.168.31.101"
113
+ port = 19132
114
+ ```
115
+
116
+ 空目录只需上述配置,无需执行 `mcpy init`;已有 Addon 项目也可添加此表。当前使用未认证连接,不读取登录身份或 token,不保证服务器允许进服;项目依赖仍会校验,但暂不装配本地 Mod。
117
+
118
+ 网络模式前台输出日志并显示日志文件路径,按 Ctrl+C 结束本次游戏。暂不支持 Map 项目、GUI、`--detach`、`--new` 或本地世界实例 ID。
119
+
100
120
  ## AI Agent 使用
101
121
 
102
122
  仓库提供标准 [mcpywrap Skill](https://github.com/EaseCation/mcpywrap/tree/main/skills/mcpywrap),帮助 Agent 安装工具、管理依赖、
@@ -109,7 +129,7 @@ mcpy run --engine-version 3.10.0.420447
109
129
  也可以下载仓库 ZIP,将完整的 `skills/mcpywrap` 文件夹复制到对应 Agent 的技能目录。
110
130
  无需从源码安装 Python 项目;CLI 和 Skill 分别安装,`pip/uv install` 不会自动注册 Skill。
111
131
  Skill 中的 `scripts/bootstrap.ps1` 可使用已有 uv 安装 CLI,并检查是否具备所需命令能力。
112
- 请使用 mcpywrap 0.3.2 或更高版本;需要固定组合时,可从 `v0.3.2` 标签安装对应 Skill。
132
+ 请使用 mcpywrap 0.3.3 或更高版本;需要固定组合时,可从 `v0.3.3` 标签安装对应 Skill。
113
133
 
114
134
  安装后可直接描述任务:
115
135
 
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """mcpywrap - 将 Python 3 代码转换为 Python 2 代码的工具"""
3
3
 
4
- __version__ = '0.3.2'
4
+ __version__ = '0.3.3'
@@ -5,6 +5,7 @@ from . import __version__
5
5
  from .command_context import OperationGroup, configure_context
6
6
 
7
7
  from .commands.run_cmd import run_cmd
8
+ from .commands.connect_cmd import connect_cmd
8
9
  from .commands.init_cmd import init_cmd
9
10
  from .commands.add_cmd import add_cmd
10
11
  from .commands.remove_cmd import remove_cmd
@@ -50,6 +51,7 @@ cli.add_command(package_cmd, name='package')
50
51
  cli.add_command(publish_cmd, name='publish')
51
52
  cli.add_command(mod_cmd, name='mod')
52
53
  cli.add_command(run_cmd, name='run')
54
+ cli.add_command(connect_cmd, name='connect')
53
55
  cli.add_command(edit_cmd, name='edit')
54
56
  cli.add_command(ui_cmd, name='ui')
55
57
  cli.add_command(doctor_cmd, name='doctor')
@@ -0,0 +1,18 @@
1
+ """无项目、无登录身份的临时网络连接。"""
2
+ import click
3
+
4
+ from ..command_context import OperationCommand
5
+ from ..mcstudio.discovery import engine_options
6
+ from ..mcstudio.network import ServerTarget, run_network
7
+
8
+
9
+ @click.command(cls=OperationCommand)
10
+ @engine_options
11
+ @click.argument('host')
12
+ @click.option('--port', type=click.IntRange(1, 65535), default=19132, show_default=True)
13
+ def connect_cmd(host, port, **engine_overrides):
14
+ """尝试未认证连接服务器;前台采集日志,Ctrl+C 结束本次游戏。
15
+
16
+ 不读取项目配置或登录身份,不装配本地 Mod,不保证服务器接受连接。
17
+ """
18
+ return run_network(ServerTarget(host, port), engine_overrides=engine_overrides)
@@ -142,6 +142,9 @@ def _run_game_with_instance(config_path, level_id, all_packs, wait=True, log_cal
142
142
  from ..dependencies import read_project
143
143
  project_dir = str(Path(config_path).resolve().parent.parent)
144
144
  config = read_project(project_dir)
145
+ if 'server' in config.get('tool', {}).get('mcpywrap', {}):
146
+ # GUI/旧会话也不能将网络目标误启动成本地世界。
147
+ raise ValueError('服务器目标请通过 CLI mcpy run 启动,不能使用本地世界实例')
145
148
  project_type = config.get('tool', {}).get('mcpywrap', {}).get('project_type', 'addon')
146
149
  project_name = config.get('project', {}).get('name', 'project')
147
150
  all_packs = _setup_dependencies(project_name, project_dir)
@@ -337,7 +340,11 @@ def _gen_random_port():
337
340
  def run_cmd(new, list, delete, force, clean_all, instance_prefix, no_gui, detach, **engine_overrides):
338
341
  """游戏实例运行与管理
339
342
 
340
- 可直接运行 'mcpy run' 启动最新实例,或使用选项管理实例
343
+ 可直接运行 'mcpy run' 启动最新实例,或使用选项管理实例。
344
+
345
+ 配置 [tool.mcpywrap.server] host/port 时改为未认证网络连接,
346
+ 不装配本地 Mod,不保证服务器接受连接。网络模式前台采集日志,
347
+ Ctrl+C 结束本次游戏,暂不支持 --detach。
341
348
  """
342
349
  base_dir = str(current_project())
343
350
  if (delete or clean_all) and non_interactive() and not force:
@@ -346,6 +353,21 @@ def run_cmd(new, list, delete, force, clean_all, instance_prefix, no_gui, detach
346
353
  if not config_exists():
347
354
  require_project()
348
355
 
356
+ # 在存档选择、配置同步和目录创建之前分流;管理选项仍只管理本地存档。
357
+ if not (list or delete or clean_all):
358
+ from ..dependencies import read_project
359
+ from ..mcstudio.network import configured_target, prepare_project, run_network
360
+ config = read_project(base_dir)
361
+ target = configured_target(config)
362
+ if target is not None:
363
+ if new or instance_prefix:
364
+ raise click.UsageError('网络模式不支持 --new 或本地世界实例 ID')
365
+ if detach:
366
+ raise click.UsageError('网络模式暂不支持 --detach;请在终端会话中前台运行')
367
+ packs = prepare_project(base_dir, config)
368
+ return run_network(target, project_dir=base_dir, packs=packs,
369
+ engine_overrides=engine_overrides)
370
+
349
371
  # 确保 map 项目的 setuptools 配置同步
350
372
  ensure_map_setuptools_sync(interactive=False)
351
373
 
@@ -18,6 +18,10 @@ def ui_cmd():
18
18
  # 检查项目是否已初始化
19
19
  if not config_exists():
20
20
  require_project()
21
+
22
+ from ..dependencies import read_project
23
+ if 'server' in read_project(current_project()).get('tool', {}).get('mcpywrap', {}):
24
+ raise click.UsageError('服务器目标暂不支持 GUI,请使用 mcpy run')
21
25
 
22
26
  from ..ui.project_ui import show_run_ui
23
27
  # 显示图形界面
@@ -96,7 +96,7 @@ def fixed_drives():
96
96
  if mask & (1 << i) and kernel.GetDriveTypeW(ctypes.c_wchar_p(f'{chr(65 + i)}:\\')) == 3]
97
97
 
98
98
 
99
- def discovery_options(project_dir=None, overrides=None, environ=None, cwd=None):
99
+ def discovery_options(project_dir=None, overrides=None, environ=None, cwd=None, read_project_config=True):
100
100
  """逐字段合并 CLI、环境变量、项目配置,并保留各自路径基准。"""
101
101
  cwd = Path(cwd or os.getcwd()).resolve()
102
102
  project_dir = Path(project_dir or cwd).resolve()
@@ -105,7 +105,7 @@ def discovery_options(project_dir=None, overrides=None, environ=None, cwd=None):
105
105
  config_file = project_dir / 'pyproject.toml'
106
106
  config = {}
107
107
  try:
108
- if config_file.exists():
108
+ if read_project_config and config_file.exists():
109
109
  with config_file.open('rb') as stream:
110
110
  document = tomli.load(stream)
111
111
  config = document.get('tool', {}).get('mcpywrap', {})
@@ -187,10 +187,10 @@ def _download_from_exe(exe):
187
187
  return None
188
188
 
189
189
 
190
- def discover_engines(project_dir=None, overrides=None, instance_version=None):
190
+ def discover_engines(project_dir=None, overrides=None, instance_version=None, *, read_project_config=True):
191
191
  result = DiscoveryResult()
192
192
  try:
193
- options = discovery_options(project_dir, overrides)
193
+ options = discovery_options(project_dir, overrides, read_project_config=read_project_config)
194
194
  requested = options.get('engine_version') or instance_version
195
195
  if requested:
196
196
  try:
@@ -0,0 +1,153 @@
1
+ """未认证网络连接:目标、项目准备与一次前台运行,不读取 MCS 身份或模板。"""
2
+ import codecs
3
+ import ipaddress
4
+ import json
5
+ import re
6
+ import subprocess
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+ from ..dependencies import DependencyService, addon_directories, read_project
14
+ from .discovery import discover_engines, require_resources
15
+ from .file_logs import FileLogServer
16
+ from .game import open_game
17
+ from .mcs import is_windows
18
+ from .runtime_cppconfig import gen_runtime_config
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ServerTarget:
23
+ host: str
24
+ port: int = 19132
25
+
26
+ def __post_init__(self):
27
+ if not isinstance(self.host, str) or not self.host or self.host != self.host.strip():
28
+ raise ValueError('server.host 必须是非空 IP 或主机名,不能包含首尾空白')
29
+ try:
30
+ ipaddress.ip_address(self.host)
31
+ except ValueError:
32
+ try:
33
+ hostname = self.host.rstrip('.').encode('idna').decode('ascii')
34
+ except UnicodeError as exc:
35
+ raise ValueError('server.host 不是有效的主机名') from exc
36
+ if len(hostname) > 253 or any(
37
+ not re.fullmatch(r'[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?', label)
38
+ for label in hostname.split('.')
39
+ ):
40
+ raise ValueError('server.host 必须是 IP 或主机名;端口请单独配置')
41
+ if type(self.port) is not int or not 1 <= self.port <= 65535:
42
+ raise ValueError('server.port 必须是 1–65535 的整数')
43
+
44
+
45
+ def configured_target(config):
46
+ settings = config.get('tool', {}).get('mcpywrap', {})
47
+ if 'server' not in settings:
48
+ return None
49
+ server = settings['server']
50
+ if not isinstance(server, dict):
51
+ raise ValueError('tool.mcpywrap.server 必须是 TOML 表')
52
+ unknown = server.keys() - {'host', 'port'}
53
+ if unknown:
54
+ raise ValueError('未知的 server 配置字段: ' + ', '.join(sorted(unknown)))
55
+ return ServerTarget(server.get('host'), server.get('port', 19132))
56
+
57
+
58
+ def prepare_project(project_dir, config=None):
59
+ """允许无根 Addon 的连接目录;出现包结构时仍严格校验。"""
60
+ root = Path(project_dir)
61
+ config = read_project(root) if config is None else config
62
+ if config.get('tool', {}).get('mcpywrap', {}).get('project_type', 'addon') != 'addon':
63
+ raise ValueError('网络运行暂时只支持 Addon 或纯连接目录,不支持 Map 项目')
64
+ markers = {'level.dat', 'db', 'manifest.json', 'pack_manifest.json'}
65
+ has_structure = any(p.name in markers or p.name.startswith(
66
+ ('behavior_pack', 'BehaviorPack', 'resource_pack', 'ResourcePack')) for p in root.iterdir())
67
+ folders = addon_directories(root) if has_structure else {}
68
+ manager = DependencyService(root).resolve(config)
69
+ packs = list(manager.get_all_dependencies().values())
70
+ if folders:
71
+ pack = manager.root_node.addon_pack
72
+ pack.behavior_pack_dir = folders.get('behavior')
73
+ pack.resource_pack_dir = folders.get('resource')
74
+ packs.append(pack)
75
+ return packs
76
+
77
+
78
+ def unauthenticated_config(engine, target):
79
+ """从本地模式的无身份默认值构造实验配置,不复制账号或伪造认证字段。"""
80
+ config = gen_runtime_config(engine.version, '', '', engine.download_dir, '', [], [])
81
+ config.update(world_info=None, client_type=1, render_engine=0,
82
+ misc={'multiplayer_game_type': 100})
83
+ config['room_info'].update(ip=target.host, port=target.port)
84
+ return config
85
+
86
+
87
+ def run_network(target, *, project_dir=None, packs=(), engine_overrides=None):
88
+ """共用调度入口;packs 留待网络装配支持,本版只报告数量,不挂载。"""
89
+ if not is_windows():
90
+ raise ValueError('网络游戏启动仅支持 Windows')
91
+ engine = discover_engines(project_dir, engine_overrides,
92
+ read_project_config=project_dir is not None).require_engine()
93
+ require_resources(engine)
94
+ config = unauthenticated_config(engine, target)
95
+ click.echo('未认证网络连接:不使用账号/token,服务器可能拒绝连接。')
96
+ click.echo(f'本次仅连接服务器,工具未装配本地 Addon(已解析 {len(packs)} 个)。')
97
+ directory = Path(tempfile.mkdtemp(prefix='mcpy-network-'))
98
+ config_path, log_path = directory / 'runtime.cppconfig', directory / 'game.log'
99
+ engine_log = directory / 'engine.log'
100
+ process, receiver = None, None
101
+ tails = []
102
+
103
+ def relay(final=False):
104
+ for tail, decoder in tails:
105
+ click.echo(decoder.decode(tail.read(), final=final), nl=False)
106
+
107
+ try:
108
+ config_path.write_text(json.dumps(config, ensure_ascii=False), encoding='utf-8')
109
+ receiver = FileLogServer(log_path)
110
+ receiver.start()
111
+ engine_log.touch()
112
+ for path in (log_path, engine_log):
113
+ tails.append((path.open('rb'), codecs.getincrementaldecoder('utf-8')(errors='replace')))
114
+ click.echo(f'目标: {target.host}:{target.port}\n日志: {log_path}\n引擎输出: {engine_log}')
115
+ process = open_game(str(config_path), engine=engine, logging_ip='127.0.0.1',
116
+ logging_port=receiver.port, wait=False, use_system_color=False,
117
+ output_path=str(engine_log))
118
+ if not process:
119
+ raise click.ClickException(f'游戏启动失败;日志目录: {directory}')
120
+ click.echo(f'游戏进程已创建,PID: {process.pid}(尚未确认连接);Ctrl+C 结束本次游戏。')
121
+ while True:
122
+ relay()
123
+ try:
124
+ code = process.wait(timeout=0.2)
125
+ break
126
+ except subprocess.TimeoutExpired:
127
+ continue
128
+ finally:
129
+ try:
130
+ if process and process.poll() is None:
131
+ process.terminate()
132
+ try:
133
+ process.wait(timeout=5)
134
+ except subprocess.TimeoutExpired:
135
+ process.kill()
136
+ process.wait(timeout=5)
137
+ finally:
138
+ try:
139
+ if receiver:
140
+ receiver.close()
141
+ relay(final=True)
142
+ finally:
143
+ for tail, _ in tails:
144
+ tail.close()
145
+ config_path.unlink(missing_ok=True)
146
+ if code:
147
+ error = click.ClickException(f'游戏退出码 {code};日志: {log_path};引擎输出: {engine_log}')
148
+ error.exit_code = code if 0 < code < 256 else 1
149
+ raise error
150
+ return {'application': 'game', 'pid': process.pid, 'state': 'exited', 'exit_code': code,
151
+ 'authenticated': False, 'connection_verified': False, 'addons_assembled': False,
152
+ 'host': target.host, 'port': target.port, 'log_path': str(log_path),
153
+ 'engine_log_path': str(engine_log), 'engine_version': engine.version}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpywrap
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A wrapper for Minecraft China Edition Addons management
5
5
  Author-email: boybook <boybook@easecation.net>
6
6
  License-Expression: MIT
@@ -96,7 +96,7 @@ mcpy add --path "../shared-addon"
96
96
  | `mcpy run -l` | 查看已有实例 |
97
97
  | `mcpy run -d <ID前缀>` | 删除指定实例 |
98
98
 
99
- `mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
99
+ 本地世界模式下,`mcpy run` 默认复用最近创建的实例。构建时主项目内容优先于依赖;修改依赖声明后,请重新启动 `mcpy dev`。
100
100
 
101
101
  ### 打包分发
102
102
 
@@ -124,6 +124,26 @@ mcpy run --engine-version 3.10.0.420447
124
124
 
125
125
  已有实例默认保留原引擎版本;显式指定版本可以切换。自定义路径、项目级设置和环境变量的用法见[引擎配置参考](https://github.com/EaseCation/mcpywrap/blob/main/docs/engine-discovery.md)。
126
126
 
127
+ ### 连接服务器(实验性)
128
+
129
+ 临时连接无需项目配置,地址可以是 IP 或主机名,端口默认 `19132`:
130
+
131
+ ```powershell
132
+ mcpy connect 192.168.31.101 --port 19132
133
+ ```
134
+
135
+ 固定目标可写入 `pyproject.toml`,随后在该目录执行 `mcpy run`:
136
+
137
+ ```toml
138
+ [tool.mcpywrap.server]
139
+ host = "192.168.31.101"
140
+ port = 19132
141
+ ```
142
+
143
+ 空目录只需上述配置,无需执行 `mcpy init`;已有 Addon 项目也可添加此表。当前使用未认证连接,不读取登录身份或 token,不保证服务器允许进服;项目依赖仍会校验,但暂不装配本地 Mod。
144
+
145
+ 网络模式前台输出日志并显示日志文件路径,按 Ctrl+C 结束本次游戏。暂不支持 Map 项目、GUI、`--detach`、`--new` 或本地世界实例 ID。
146
+
127
147
  ## AI Agent 使用
128
148
 
129
149
  仓库提供标准 [mcpywrap Skill](https://github.com/EaseCation/mcpywrap/tree/main/skills/mcpywrap),帮助 Agent 安装工具、管理依赖、
@@ -136,7 +156,7 @@ mcpy run --engine-version 3.10.0.420447
136
156
  也可以下载仓库 ZIP,将完整的 `skills/mcpywrap` 文件夹复制到对应 Agent 的技能目录。
137
157
  无需从源码安装 Python 项目;CLI 和 Skill 分别安装,`pip/uv install` 不会自动注册 Skill。
138
158
  Skill 中的 `scripts/bootstrap.ps1` 可使用已有 uv 安装 CLI,并检查是否具备所需命令能力。
139
- 请使用 mcpywrap 0.3.2 或更高版本;需要固定组合时,可从 `v0.3.2` 标签安装对应 Skill。
159
+ 请使用 mcpywrap 0.3.3 或更高版本;需要固定组合时,可从 `v0.3.3` 标签安装对应 Skill。
140
160
 
141
161
  安装后可直接描述任务:
142
162
 
@@ -32,6 +32,7 @@ mcpywrap/builders/watcher.py
32
32
  mcpywrap/commands/__init__.py
33
33
  mcpywrap/commands/add_cmd.py
34
34
  mcpywrap/commands/build_cmd.py
35
+ mcpywrap/commands/connect_cmd.py
35
36
  mcpywrap/commands/default_cmd.py
36
37
  mcpywrap/commands/dependency_prompt.py
37
38
  mcpywrap/commands/dev_cmd.py
@@ -55,6 +56,7 @@ mcpywrap/mcstudio/file_logs.py
55
56
  mcpywrap/mcstudio/game.py
56
57
  mcpywrap/mcstudio/log_protocol.py
57
58
  mcpywrap/mcstudio/mcs.py
59
+ mcpywrap/mcstudio/network.py
58
60
  mcpywrap/mcstudio/processes.py
59
61
  mcpywrap/mcstudio/runtime_cppconfig.py
60
62
  mcpywrap/mcstudio/session_worker.py
@@ -89,4 +91,5 @@ tests/test_automation.py
89
91
  tests/test_engine_discovery.py
90
92
  tests/test_game_window_skill.py
91
93
  tests/test_local_dependencies.py
94
+ tests/test_network.py
92
95
  tests/test_package.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mcpywrap"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "A wrapper for Minecraft China Edition Addons management"
9
9
  readme = "README.md"
10
10
  authors = [{name = "boybook", email = "boybook@easecation.net"}]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mcpywrap
3
- description: 使用 mcpywrap 管理《我的世界》中国版 Addon/地图项目、Python 包与本地依赖,构建分发 ZIP,启动游戏并读取日志;使用会话绑定的脚本截图和发送按键,复杂界面交互交接 Computer Use。
3
+ description: 使用 mcpywrap 管理《我的世界》中国版 Addon/地图项目与依赖,构建 ZIP,启动本地世界或尝试未认证服务器连接并读取日志;使用会话绑定的脚本截图和发送按键,复杂界面交互交接 Computer Use。
4
4
  ---
5
5
 
6
6
  # mcpywrap
@@ -9,7 +9,7 @@ description: 使用 mcpywrap 管理《我的世界》中国版 Addon/地图项
9
9
 
10
10
  ## 准备
11
11
 
12
- - 明确用户的项目绝对路径;每条命令使用 `--project`,不依赖当前目录。
12
+ - 项目操作使用绝对路径 `--project`,不依赖当前目录;临时 `connect` 无需项目。
13
13
  - 检查 `mcpy --version`、`mcpy --help` 及要调用的子命令帮助。版本号不能代替能力检查。
14
14
  - 需要安装时使用 [bootstrap.ps1](scripts/bootstrap.ps1)。脚本使用已有 uv,缺少时返回官方安装入口;默认复用已有可用安装,升级或更换来源使用 `-Upgrade`。
15
15
  - 正式版本使用 `-Version`,固定源码使用 `-GitRef <完整提交 SHA>`,开发目录使用 `-EditablePath <绝对路径>`。当前 Skill 对应的 CLI 必须支持 `--project`、`--non-interactive`、`--json` 和会话命令。
@@ -39,7 +39,7 @@ description: 使用 mcpywrap 管理《我的世界》中国版 Addon/地图项
39
39
  `dev` 持续输出文本日志,不使用 `--json`;修改依赖声明后重启监控。
40
40
  `publish --yes` 是真实上传;只有用户请求发布时才调用。
41
41
 
42
- ## 游戏验证
42
+ ## 本地世界验证
43
43
 
44
44
  1. `doctor --json` 检查引擎与资源;编辑器缺失不等于无法运行游戏。
45
45
  2. `run --no-gui --detach --json` 启动本次会话,只显示游戏本体。
@@ -51,6 +51,23 @@ description: 使用 mcpywrap 管理《我的世界》中国版 Addon/地图项
51
51
  可使用 [smoke.py](scripts/smoke.py) 自动检查并打包;`--game` 才运行游戏,
52
52
  `--expect-log` 可重复指定加载标记。该脚本会停止自己创建的游戏;不要用于要求保留游戏窗口的任务。
53
53
 
54
+ ## 网络连接(实验性)
55
+
56
+ 网络能力需 CLI 0.3.3 或更高版本,先检查 `connect --help`。临时连接用 `mcpy connect <IP或主机名> --port 19132`,不读取项目配置。
57
+ 固定目标在 `pyproject.toml` 添加以下表,再执行 `mcpy --project <绝对路径> --non-interactive run`:
58
+
59
+ ```toml
60
+ [tool.mcpywrap.server]
61
+ host = "192.168.31.101"
62
+ port = 19132
63
+ ```
64
+
65
+ 纯连接目录只需此表,无需 `init`;Addon 项目仍校验依赖,但本版不装配本地 Mod。
66
+ 不读取登录身份或 token,不能把进程创建成功当成进服成功。
67
+ 在可持续运行的终端会话中执行,读取输出的 PID 和两路日志路径;Ctrl+C 结束本次游戏。
68
+ `--json` 在游戏退出后才返回最终结果;不支持 `--detach`、`--new`、世界实例 ID、Map 或 GUI。
69
+ 网络运行不生成本地会话 ID,不使用 `status/logs/stop --session`、`smoke.py --game` 或 `game_window.py` 管理它。
70
+
54
71
  ## 游戏截图与键盘操作
55
72
 
56
73
  固定截图和键盘输入优先使用 Skill 自带的 [game_window.py](scripts/game_window.py),仅需 Windows 和 Python 标准库。
@@ -0,0 +1,228 @@
1
+ """未认证网络调度:不需要 MCS、认证服务或真实游戏。"""
2
+ import importlib
3
+ import inspect
4
+ import json
5
+ import socket
6
+ import subprocess
7
+ import tempfile
8
+ import time
9
+ import unittest
10
+ import uuid
11
+ from pathlib import Path
12
+ from unittest.mock import Mock, patch
13
+
14
+ from click.testing import CliRunner
15
+
16
+ from mcpywrap.cli import cli
17
+ from mcpywrap.dependencies import DependencyError
18
+ from mcpywrap.mcstudio import network as n
19
+ from mcpywrap.mcstudio.discovery import Engine, discovery_options
20
+
21
+
22
+ class NetworkTests(unittest.TestCase):
23
+ def setUp(self):
24
+ self.temp = tempfile.TemporaryDirectory()
25
+ self.addCleanup(self.temp.cleanup)
26
+ self.root = Path(self.temp.name)
27
+ self.project = self.root / '中文 项目'
28
+ self.project.mkdir()
29
+ options = {'mix_stderr': False} if 'mix_stderr' in inspect.signature(CliRunner).parameters else {}
30
+ self.runner = CliRunner(**options)
31
+
32
+ def config(self, text='[tool.mcpywrap.server]\nhost="127.0.0.1"\n'):
33
+ (self.project / 'pyproject.toml').write_text(text, encoding='utf-8')
34
+
35
+ def call(self, *args):
36
+ return self.runner.invoke(cli, ['--project', str(self.project), '--non-interactive', *args, '--json'])
37
+
38
+ def addon(self, root):
39
+ pack = root / 'behavior_pack_demo'
40
+ pack.mkdir(parents=True)
41
+ (pack / 'manifest.json').write_text(json.dumps({
42
+ 'header': {'uuid': str(uuid.uuid4()), 'name': 'test', 'version': [1, 0, 0]},
43
+ 'modules': [{'uuid': str(uuid.uuid4()), 'type': 'data', 'version': [1, 0, 0]}]}))
44
+
45
+ def test_target_validation(self):
46
+ for host in ('127.0.0.1', '::1', 'example.com', '中文.example'):
47
+ self.assertEqual(n.ServerTarget(host).port, 19132)
48
+ for host in ('', None, ' x', 'a:123', 'http://host', 'a/b', 'a\n', '-host'):
49
+ with self.subTest(host=host), self.assertRaises(ValueError):
50
+ n.ServerTarget(host)
51
+ for port in (True, '19132', 0, 65536, 1.5):
52
+ with self.assertRaises(ValueError):
53
+ n.ServerTarget('localhost', port)
54
+ for value in ({}, None, 'host', {'host': 'a', 'prot': 10}):
55
+ with self.assertRaises(ValueError):
56
+ n.configured_target({'tool': {'mcpywrap': {'server': value}}})
57
+ self.assertIsNone(n.configured_target({}))
58
+
59
+ def test_empty_project_and_dependency_only_and_root(self):
60
+ self.config()
61
+ self.assertEqual(n.prepare_project(self.project), [])
62
+ dep = self.root / '共享 addon'
63
+ self.addon(dep)
64
+ self.config('[tool.mcpywrap]\nlocal_dependencies=["../共享 addon"]\n'
65
+ '[tool.mcpywrap.server]\nhost="localhost"\n')
66
+ packs = n.prepare_project(self.project)
67
+ self.assertEqual(len(packs), 1)
68
+ self.assertTrue(Path(packs[0].path).samefile(dep))
69
+ self.addon(self.project)
70
+ self.assertEqual(len(n.prepare_project(self.project)), 2)
71
+ self.assertFalse((dep / 'pyproject.toml').exists())
72
+
73
+ def test_bad_structure_is_not_treated_as_empty(self):
74
+ for marker in ('behavior_pack', 'resource_pack', 'level.dat', 'manifest.json'):
75
+ path = self.project / marker
76
+ path.touch()
77
+ with self.subTest(marker=marker), self.assertRaises(DependencyError):
78
+ n.prepare_project(self.project)
79
+ path.unlink()
80
+ self.config('[tool.mcpywrap]\nlocal_dependencies=["../missing"]\n')
81
+ with self.assertRaises(DependencyError):
82
+ n.prepare_project(self.project)
83
+ self.config('[tool.mcpywrap]\nproject_type="map"\n')
84
+ with self.assertRaises(ValueError):
85
+ n.prepare_project(self.project)
86
+ self.config('[project]\ndependencies=["mcpy-nonexistent-test-package>=9"]\n')
87
+ with self.assertRaises(DependencyError):
88
+ n.prepare_project(self.project)
89
+
90
+ def test_cli_run_routes_before_local_side_effects(self):
91
+ self.config()
92
+ before = (self.project / 'pyproject.toml').read_bytes()
93
+ with patch.object(n, 'run_network', return_value={'authenticated': False}) as launch:
94
+ result = self.call('run')
95
+ self.assertEqual(result.exit_code, 0, result.output)
96
+ self.assertEqual(launch.call_args.args[0], n.ServerTarget('127.0.0.1'))
97
+ self.assertEqual(launch.call_args.kwargs['packs'], [])
98
+ self.assertFalse((self.project / '.runtime').exists())
99
+ self.assertEqual(before, (self.project / 'pyproject.toml').read_bytes())
100
+
101
+ def test_cli_connect_ignores_project_and_requires_host(self):
102
+ self.config('invalid = [')
103
+ with patch('mcpywrap.commands.connect_cmd.run_network', return_value={}) as launch:
104
+ result = self.call('connect', 'localhost', '--port', '20000')
105
+ self.assertEqual(result.exit_code, 0, result.output)
106
+ self.assertEqual(launch.call_args.args[0], n.ServerTarget('localhost', 20000))
107
+ self.assertNotIn('project_dir', launch.call_args.kwargs)
108
+ self.assertEqual(self.call('connect').exit_code, 2)
109
+ self.assertEqual(self.call('connect', 'localhost', '--port', '0').exit_code, 2)
110
+ self.assertFalse((self.project / '.runtime').exists())
111
+ self.assertEqual(discovery_options(self.project, environ={}, read_project_config=False), {})
112
+
113
+ def test_network_options_and_invalid_config_never_start_world(self):
114
+ run = importlib.import_module('mcpywrap.commands.run_cmd')
115
+ self.config()
116
+ with patch.object(run, '_run_game_with_instance') as local, patch.object(n, 'run_network') as launch:
117
+ for args in (('run', '--new'), ('run', '1234'), ('run', '--detach')):
118
+ self.assertEqual(self.call(*args).exit_code, 2)
119
+ self.config('[tool.mcpywrap.server]\nport=123\n')
120
+ self.assertEqual(self.call('run').exit_code, 1)
121
+ self.config('broken = [')
122
+ self.assertEqual(self.call('run').exit_code, 1)
123
+ local.assert_not_called()
124
+ launch.assert_not_called()
125
+ self.assertFalse((self.project / '.runtime').exists())
126
+
127
+ def test_gui_and_stale_worker_guard(self):
128
+ self.config()
129
+ ui = importlib.import_module('mcpywrap.commands.ui_cmd')
130
+ with patch.object(ui, 'non_interactive', return_value=False):
131
+ result = self.runner.invoke(cli, ['--project', str(self.project), 'ui'])
132
+ self.assertNotEqual(result.exit_code, 0)
133
+ self.assertIn('mcpy run', result.stderr)
134
+ run = importlib.import_module('mcpywrap.commands.run_cmd')
135
+ with self.assertRaisesRegex(ValueError, '服务器目标'):
136
+ run._run_game_with_instance(str(self.project / '.runtime/old.cppconfig'), 'old', [])
137
+
138
+ def test_anonymous_config_does_not_inherit_identity(self):
139
+ engine = Engine('game', '3.10.0.420447', 'engine', str(self.root), 'test')
140
+ config = n.unauthenticated_config(engine, n.ServerTarget('localhost'))
141
+ self.assertIsNone(config['world_info'])
142
+ self.assertEqual(config['room_info']['token'], '')
143
+ self.assertEqual(config['misc'], {'multiplayer_game_type': 100})
144
+ self.assertNotIn('player_info', config)
145
+ self.assertEqual(config['LocalComponentPathsDict'], {})
146
+
147
+ def execute_fake(self, *, exit_code=0, interrupt=False, failed_start=False):
148
+ directory = self.root / 'run output'
149
+ directory.mkdir()
150
+ process = Mock(pid=12345)
151
+ process.poll.return_value = 0
152
+ process.wait.return_value = exit_code
153
+ if interrupt:
154
+ process.poll.return_value = None
155
+ process.wait.side_effect = [KeyboardInterrupt(), None]
156
+ engine = Engine('game', '3.10.0.420447', 'engine', str(self.root), 'test')
157
+
158
+ def launch(path, **kwargs):
159
+ config = json.loads(Path(path).read_text(encoding='utf-8'))
160
+ self.assertIsNone(config['world_info'])
161
+ self.assertEqual(kwargs['logging_ip'], '127.0.0.1')
162
+ Path(kwargs['output_path']).write_text('native engine output\n', encoding='utf-8')
163
+ with socket.create_connection(('127.0.0.1', kwargs['logging_port']), timeout=2) as client:
164
+ client.sendall('网络日志\n'.encode('utf-8'))
165
+ deadline = time.monotonic() + 2
166
+ while time.monotonic() < deadline and not (directory / 'game.log').read_text(encoding='utf-8'):
167
+ time.sleep(0.01)
168
+ return False if failed_start else process
169
+
170
+ with patch.object(n, 'is_windows', return_value=True), \
171
+ patch.object(n, 'discover_engines', return_value=Mock(require_engine=lambda: engine)) as discover, \
172
+ patch.object(n, 'require_resources'), \
173
+ patch.object(n.tempfile, 'mkdtemp', return_value=str(directory)), \
174
+ patch.object(n, 'open_game', side_effect=launch):
175
+ try:
176
+ result = n.run_network(n.ServerTarget('localhost'))
177
+ self.assertFalse(result['connection_verified'])
178
+ self.assertFalse(result['authenticated'])
179
+ finally:
180
+ self.assertFalse((directory / 'runtime.cppconfig').exists())
181
+ self.assertEqual((directory / 'game.log').read_text(encoding='utf-8'), '网络日志\n')
182
+ self.assertEqual((directory / 'engine.log').read_text(encoding='utf-8'), 'native engine output\n')
183
+ self.assertFalse(discover.call_args.kwargs['read_project_config'])
184
+ if interrupt:
185
+ process.terminate.assert_called_once()
186
+
187
+ def test_listener_ready_and_cleanup(self):
188
+ with patch.object(n.click, 'echo') as output:
189
+ self.execute_fake()
190
+ self.assertTrue(any('native engine output' in str(c) for c in output.call_args_list))
191
+ self.assertTrue(any('网络日志' in str(c) for c in output.call_args_list))
192
+
193
+ def test_cli_nonzero_exit_and_structured_result(self):
194
+ import click
195
+ failure = click.ClickException('game exited 7')
196
+ failure.exit_code = 7
197
+ with patch('mcpywrap.commands.connect_cmd.run_network', side_effect=failure):
198
+ result = self.call('connect', 'localhost')
199
+ self.assertEqual(result.exit_code, 7)
200
+ self.assertFalse(json.loads(result.stdout)['ok'])
201
+
202
+ def test_interrupt_stops_only_owned_process(self):
203
+ with self.assertRaises(KeyboardInterrupt):
204
+ self.execute_fake(interrupt=True)
205
+
206
+ def test_nonzero_and_failed_start(self):
207
+ import click
208
+ with self.assertRaises(click.ClickException) as raised:
209
+ self.execute_fake(exit_code=7)
210
+ self.assertEqual(raised.exception.exit_code, 7)
211
+
212
+ def test_failed_start_cleanup(self):
213
+ import click
214
+ with self.assertRaises(click.ClickException):
215
+ self.execute_fake(failed_start=True)
216
+
217
+ def test_engine_validation_precedes_writes(self):
218
+ with patch.object(n, 'is_windows', return_value=True), \
219
+ patch.object(n, 'discover_engines', side_effect=ValueError('missing engine')), \
220
+ patch.object(n.tempfile, 'mkdtemp') as create, patch.object(n, 'open_game') as launch:
221
+ with self.assertRaises(ValueError):
222
+ n.run_network(n.ServerTarget('localhost'))
223
+ create.assert_not_called()
224
+ launch.assert_not_called()
225
+
226
+
227
+ if __name__ == '__main__':
228
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes