mcpywrap-core 0.6.0__py3-none-any.whl
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_core/__init__.py +2 -0
- mcpywrap_core/__main__.py +18 -0
- mcpywrap_core/_script_worker.py +278 -0
- mcpywrap_core/_version.py +1 -0
- mcpywrap_core/api.py +69 -0
- mcpywrap_core/builders/AddonsPack.py +310 -0
- mcpywrap_core/builders/MapPack.py +171 -0
- mcpywrap_core/builders/__init__.py +20 -0
- mcpywrap_core/builders/assembly.py +100 -0
- mcpywrap_core/builders/dependency_manager.py +239 -0
- mcpywrap_core/builders/file_merge.py +260 -0
- mcpywrap_core/builders/project_builder.py +182 -0
- mcpywrap_core/builders/watcher.py +162 -0
- mcpywrap_core/cli.py +75 -0
- mcpywrap_core/code_libraries.py +201 -0
- mcpywrap_core/command_context.py +242 -0
- mcpywrap_core/commands/__init__.py +1 -0
- mcpywrap_core/commands/add_cmd.py +76 -0
- mcpywrap_core/commands/build_cmd.py +15 -0
- mcpywrap_core/commands/connect_cmd.py +46 -0
- mcpywrap_core/commands/default_cmd.py +14 -0
- mcpywrap_core/commands/dependency_prompt.py +45 -0
- mcpywrap_core/commands/deployment_cmd.py +115 -0
- mcpywrap_core/commands/deployment_output.py +289 -0
- mcpywrap_core/commands/desktop_cmd.py +104 -0
- mcpywrap_core/commands/dev_cmd.py +108 -0
- mcpywrap_core/commands/doctor_cmd.py +32 -0
- mcpywrap_core/commands/edit_cmd.py +109 -0
- mcpywrap_core/commands/engine_cmd.py +90 -0
- mcpywrap_core/commands/init_cmd.py +48 -0
- mcpywrap_core/commands/mod_cmd.py +53 -0
- mcpywrap_core/commands/modsdk_cmd.py +41 -0
- mcpywrap_core/commands/package_cmd.py +16 -0
- mcpywrap_core/commands/publish_cmd.py +51 -0
- mcpywrap_core/commands/record_cmd.py +85 -0
- mcpywrap_core/commands/remove_cmd.py +62 -0
- mcpywrap_core/commands/run_cmd.py +169 -0
- mcpywrap_core/commands/runtime_cmd.py +150 -0
- mcpywrap_core/commands/runtime_input_cmd.py +86 -0
- mcpywrap_core/commands/runtime_player_cmd.py +192 -0
- mcpywrap_core/commands/runtime_ui_cmd.py +121 -0
- mcpywrap_core/commands/script_cmd.py +54 -0
- mcpywrap_core/commands/serve_cmd.py +39 -0
- mcpywrap_core/commands/session_cmd.py +95 -0
- mcpywrap_core/commands/sync_cmd.py +29 -0
- mcpywrap_core/commands/ui_cmd.py +36 -0
- mcpywrap_core/config.py +254 -0
- mcpywrap_core/consumer_prepare.py +471 -0
- mcpywrap_core/core-contract.json +6 -0
- mcpywrap_core/custom_packaging.py +41 -0
- mcpywrap_core/dependencies.py +326 -0
- mcpywrap_core/engines/__init__.py +1 -0
- mcpywrap_core/engines/android_icon.py +186 -0
- mcpywrap_core/engines/apk_profile.py +90 -0
- mcpywrap_core/engines/backend.py +134 -0
- mcpywrap_core/engines/host.py +32 -0
- mcpywrap_core/engines/install.py +665 -0
- mcpywrap_core/engines/macos.py +483 -0
- mcpywrap_core/engines/macos_icons.py +159 -0
- mcpywrap_core/engines/macos_network.py +58 -0
- mcpywrap_core/engines/official.py +129 -0
- mcpywrap_core/engines/preferences.py +146 -0
- mcpywrap_core/engines/tui.py +110 -0
- mcpywrap_core/engines/windows.py +144 -0
- mcpywrap_core/framework_presets.py +22 -0
- mcpywrap_core/frameworks.py +77 -0
- mcpywrap_core/git_cache.py +154 -0
- mcpywrap_core/git_projects.py +266 -0
- mcpywrap_core/host.py +93 -0
- mcpywrap_core/host_assets.py +14 -0
- mcpywrap_core/input_executor.py +246 -0
- mcpywrap_core/input_plan.py +257 -0
- mcpywrap_core/mcstudio/SimpleMonitor.py +51 -0
- mcpywrap_core/mcstudio/__init__.py +1 -0
- mcpywrap_core/mcstudio/auth_interaction.py +88 -0
- mcpywrap_core/mcstudio/bridge_assets.py +85 -0
- mcpywrap_core/mcstudio/deployment_links.py +670 -0
- mcpywrap_core/mcstudio/diagnostics.py +21 -0
- mcpywrap_core/mcstudio/discovery.py +337 -0
- mcpywrap_core/mcstudio/editor.py +81 -0
- mcpywrap_core/mcstudio/file_logs.py +66 -0
- mcpywrap_core/mcstudio/game.py +256 -0
- mcpywrap_core/mcstudio/host_input.py +252 -0
- mcpywrap_core/mcstudio/hot_reload.py +218 -0
- mcpywrap_core/mcstudio/image.py +20 -0
- mcpywrap_core/mcstudio/input_sequence.py +204 -0
- mcpywrap_core/mcstudio/launcher_control.py +56 -0
- mcpywrap_core/mcstudio/launcher_python.py +134 -0
- mcpywrap_core/mcstudio/log_colorizer.py +252 -0
- mcpywrap_core/mcstudio/log_protocol.py +74 -0
- mcpywrap_core/mcstudio/log_stream.py +136 -0
- mcpywrap_core/mcstudio/mcs.py +149 -0
- mcpywrap_core/mcstudio/mcs_auth.py +196 -0
- mcpywrap_core/mcstudio/network.py +187 -0
- mcpywrap_core/mcstudio/private_logs.py +70 -0
- mcpywrap_core/mcstudio/processes.py +41 -0
- mcpywrap_core/mcstudio/recording_worker.py +154 -0
- mcpywrap_core/mcstudio/recordings.py +351 -0
- mcpywrap_core/mcstudio/runtime_cppconfig.py +249 -0
- mcpywrap_core/mcstudio/runtime_debug.py +437 -0
- mcpywrap_core/mcstudio/runtime_input_payload.py +277 -0
- mcpywrap_core/mcstudio/runtime_key_timeline_payload.py +229 -0
- mcpywrap_core/mcstudio/runtime_player_payload.py +713 -0
- mcpywrap_core/mcstudio/runtime_ui.py +212 -0
- mcpywrap_core/mcstudio/runtime_ui_outline.py +424 -0
- mcpywrap_core/mcstudio/runtime_ui_payload.py +560 -0
- mcpywrap_core/mcstudio/screenshot.py +94 -0
- mcpywrap_core/mcstudio/session_capabilities.py +54 -0
- mcpywrap_core/mcstudio/session_worker.py +177 -0
- mcpywrap_core/mcstudio/sessions.py +285 -0
- mcpywrap_core/mcstudio/studio_server.py +363 -0
- mcpywrap_core/mcstudio/studio_server_ui.py +126 -0
- mcpywrap_core/mcstudio/symlink_helper_global.py +75 -0
- mcpywrap_core/mcstudio/symlink_helper_map.py +114 -0
- mcpywrap_core/mcstudio/symlinks.py +534 -0
- mcpywrap_core/mcstudio/unified_input.py +39 -0
- mcpywrap_core/mcstudio/window.py +755 -0
- mcpywrap_core/minecraft/__init__.py +1 -0
- mcpywrap_core/minecraft/addons.py +148 -0
- mcpywrap_core/minecraft/level_dat.py +958 -0
- mcpywrap_core/minecraft/map.py +85 -0
- mcpywrap_core/minecraft/modsdk_environment.py +197 -0
- mcpywrap_core/minecraft/netease_modsdk.py +269 -0
- mcpywrap_core/minecraft/template/generate_mod_files.py +116 -0
- mcpywrap_core/minecraft/template/mod_template.py +494 -0
- mcpywrap_core/operations/__init__.py +1 -0
- mcpywrap_core/operations/build.py +66 -0
- mcpywrap_core/operations/package.py +97 -0
- mcpywrap_core/operations/sync.py +82 -0
- mcpywrap_core/prepare_environment.py +149 -0
- mcpywrap_core/project_dependencies.py +164 -0
- mcpywrap_core/project_init.py +73 -0
- mcpywrap_core/project_scripts.py +369 -0
- mcpywrap_core/recording_io.py +122 -0
- mcpywrap_core/remote/__init__.py +1 -0
- mcpywrap_core/remote/client.py +271 -0
- mcpywrap_core/remote/http_server.py +205 -0
- mcpywrap_core/remote/protocol.py +22 -0
- mcpywrap_core/remote/service.py +317 -0
- mcpywrap_core/runtime/__init__.py +1 -0
- mcpywrap_core/runtime/deployment.py +501 -0
- mcpywrap_core/runtime/deployment_inspection.py +645 -0
- mcpywrap_core/runtime/deployment_service.py +688 -0
- mcpywrap_core/runtime/diagnostics.py +74 -0
- mcpywrap_core/runtime/groups.py +666 -0
- mcpywrap_core/runtime/instances.py +147 -0
- mcpywrap_core/runtime/native.py +64 -0
- mcpywrap_core/runtime/occupancy.py +78 -0
- mcpywrap_core/runtime/preparation.py +97 -0
- mcpywrap_core/runtime/profiles.py +183 -0
- mcpywrap_core/runtime/storage.py +42 -0
- mcpywrap_core/runtime/targets.py +24 -0
- mcpywrap_core/runtime/watch.py +158 -0
- mcpywrap_core/runtime/windows.py +184 -0
- mcpywrap_core/source_files.py +82 -0
- mcpywrap_core/timeline.py +26 -0
- mcpywrap_core/ui/__init__.py +5 -0
- mcpywrap_core/ui/application.py +58 -0
- mcpywrap_core/ui/assets/mcpy.icns +0 -0
- mcpywrap_core/ui/assets/mcpy.icon/Assets/01-cube.svg +5 -0
- mcpywrap_core/ui/assets/mcpy.icon/Assets/02-prompt.svg +4 -0
- mcpywrap_core/ui/assets/mcpy.icon/icon.json +56 -0
- mcpywrap_core/ui/assets/mcpy.png +0 -0
- mcpywrap_core/ui/engine_setup.py +119 -0
- mcpywrap_core/ui/log_view.py +201 -0
- mcpywrap_core/ui/log_window.py +147 -0
- mcpywrap_core/ui/project_ui.py +963 -0
- mcpywrap_core/ui/run_session.py +89 -0
- mcpywrap_core/ui/session_controller.py +416 -0
- mcpywrap_core/ui/session_tools.py +76 -0
- mcpywrap_core/ui/session_views.py +236 -0
- mcpywrap_core/ui/world_config.py +112 -0
- mcpywrap_core/utils/__init__.py +6 -0
- mcpywrap_core/utils/console.py +13 -0
- mcpywrap_core/utils/link_snapshot.py +131 -0
- mcpywrap_core/utils/locks.py +85 -0
- mcpywrap_core/utils/paths.py +18 -0
- mcpywrap_core/utils/pip_error_parser.py +184 -0
- mcpywrap_core/utils/print_guide.py +30 -0
- mcpywrap_core/utils/project_setup.py +148 -0
- mcpywrap_core/utils/utils.py +38 -0
- mcpywrap_core-0.6.0.dist-info/METADATA +33 -0
- mcpywrap_core-0.6.0.dist-info/RECORD +189 -0
- mcpywrap_core-0.6.0.dist-info/WHEEL +5 -0
- mcpywrap_core-0.6.0.dist-info/licenses/LICENSE +21 -0
- mcpywrap_core-0.6.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +35 -0
- mcpywrap_core-0.6.0.dist-info/top_level.txt +2 -0
- mcpywrap_host/__init__.py +9 -0
- mcpywrap_host/store.py +276 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import click
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MapPack(object):
|
|
10
|
+
|
|
11
|
+
pkg_name: str
|
|
12
|
+
path: str
|
|
13
|
+
|
|
14
|
+
behavior_packs_dir: str
|
|
15
|
+
resource_packs_dir: str
|
|
16
|
+
|
|
17
|
+
behavior_packs = []
|
|
18
|
+
resource_packs = []
|
|
19
|
+
|
|
20
|
+
def __init__(self, pkg_name: str, path: str):
|
|
21
|
+
self.pkg_name = pkg_name
|
|
22
|
+
self.path = path
|
|
23
|
+
self.behavior_packs = []
|
|
24
|
+
self.resource_packs = []
|
|
25
|
+
|
|
26
|
+
# 读取配置文件
|
|
27
|
+
self.behavior_packs_dir = os.path.join(self.path, "behavior_packs")
|
|
28
|
+
self.resource_packs_dir = os.path.join(self.path, "resource_packs")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 遍历行为包目录
|
|
32
|
+
for pack in sorted(os.listdir(self.behavior_packs_dir)) if os.path.isdir(self.behavior_packs_dir) else []:
|
|
33
|
+
pack_path = os.path.join(self.behavior_packs_dir, pack)
|
|
34
|
+
if os.path.isdir(pack_path):
|
|
35
|
+
self.behavior_packs.append(pack_path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# 遍历资源包目录
|
|
39
|
+
for pack in sorted(os.listdir(self.resource_packs_dir)) if os.path.isdir(self.resource_packs_dir) else []:
|
|
40
|
+
pack_path = os.path.join(self.resource_packs_dir, pack)
|
|
41
|
+
if os.path.isdir(pack_path):
|
|
42
|
+
self.resource_packs.append(pack_path)
|
|
43
|
+
|
|
44
|
+
def copy_level_data_to(self, target_dir: str):
|
|
45
|
+
"""
|
|
46
|
+
复制地图数据到目标目录
|
|
47
|
+
"""
|
|
48
|
+
# level.dat
|
|
49
|
+
level_dat_path = os.path.join(target_dir, "level.dat")
|
|
50
|
+
if not os.path.exists(level_dat_path):
|
|
51
|
+
origin_level_dat_path = os.path.join(self.path, "level.dat")
|
|
52
|
+
if os.path.exists(origin_level_dat_path):
|
|
53
|
+
shutil.copy2(origin_level_dat_path, level_dat_path)
|
|
54
|
+
# levelname.txt
|
|
55
|
+
levelname_txt_path = os.path.join(target_dir, "levelname.txt")
|
|
56
|
+
if not os.path.exists(levelname_txt_path):
|
|
57
|
+
origin_levelname_txt_path = os.path.join(self.path, "levelname.txt")
|
|
58
|
+
if os.path.exists(origin_levelname_txt_path):
|
|
59
|
+
shutil.copy2(origin_levelname_txt_path, levelname_txt_path)
|
|
60
|
+
# db
|
|
61
|
+
level_db_dir = os.path.join(target_dir, "db")
|
|
62
|
+
if not os.path.exists(level_db_dir) and os.path.exists(os.path.join(self.path, "db")):
|
|
63
|
+
shutil.copytree(os.path.join(self.path, "db"), level_db_dir)
|
|
64
|
+
|
|
65
|
+
def copy_resource_packs_to(self, target_map_dir: str):
|
|
66
|
+
"""
|
|
67
|
+
复制资源包到目标目录
|
|
68
|
+
"""
|
|
69
|
+
for pack in self.resource_packs:
|
|
70
|
+
pack_name = os.path.basename(pack)
|
|
71
|
+
target_pack_dir = os.path.join(target_map_dir, "resource_packs", pack_name)
|
|
72
|
+
# 先删除目标目录下的包
|
|
73
|
+
if os.path.exists(target_pack_dir):
|
|
74
|
+
shutil.rmtree(target_pack_dir)
|
|
75
|
+
shutil.copytree(pack, target_pack_dir)
|
|
76
|
+
|
|
77
|
+
def copy_behavior_packs_to(self, target_map_dir: str):
|
|
78
|
+
"""
|
|
79
|
+
复制行为包到目标目录
|
|
80
|
+
"""
|
|
81
|
+
for pack in self.behavior_packs:
|
|
82
|
+
pack_name = os.path.basename(pack)
|
|
83
|
+
target_pack_dir = os.path.join(target_map_dir, "behavior_packs", pack_name)
|
|
84
|
+
# 先删除目标目录下的包
|
|
85
|
+
if os.path.exists(target_pack_dir):
|
|
86
|
+
shutil.rmtree(target_pack_dir)
|
|
87
|
+
shutil.copytree(pack, target_pack_dir)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def setup_packs_symlinks_to(self, level_id: str, target_dir: str):
|
|
91
|
+
"""
|
|
92
|
+
设置行为包和资源包的符号链接到目标目录(本体到远程)
|
|
93
|
+
"""
|
|
94
|
+
from ..mcstudio.symlinks import setup_map_packs_symlinks
|
|
95
|
+
setup_map_packs_symlinks(self.path, level_id, target_dir)
|
|
96
|
+
|
|
97
|
+
def setup_world_packs_config(self):
|
|
98
|
+
"""根据当前path,生成世界需加载的pack的配置文件"""
|
|
99
|
+
behavior_packs_config = []
|
|
100
|
+
if os.path.exists(self.behavior_packs_dir):
|
|
101
|
+
behavior_packs_config = _find_and_extract_pack_info(self.behavior_packs_dir)
|
|
102
|
+
if behavior_packs_config:
|
|
103
|
+
world_behavior_packs_path = os.path.join(self.path, "world_behavior_packs.json")
|
|
104
|
+
with open(world_behavior_packs_path, 'w', encoding='utf-8') as f:
|
|
105
|
+
json.dump(behavior_packs_config, f, ensure_ascii=False, indent=4)
|
|
106
|
+
|
|
107
|
+
resource_packs_config = []
|
|
108
|
+
if os.path.exists(self.resource_packs_dir):
|
|
109
|
+
resource_packs_config = _find_and_extract_pack_info(self.resource_packs_dir)
|
|
110
|
+
if resource_packs_config:
|
|
111
|
+
world_resource_packs_path = os.path.join(self.path, "world_resource_packs.json")
|
|
112
|
+
with open(world_resource_packs_path, 'w', encoding='utf-8') as f:
|
|
113
|
+
json.dump(resource_packs_config, f, ensure_ascii=False, indent=4)
|
|
114
|
+
|
|
115
|
+
return behavior_packs_config, resource_packs_config
|
|
116
|
+
|
|
117
|
+
def _find_and_extract_pack_info(packs_dir):
|
|
118
|
+
"""
|
|
119
|
+
搜索指定目录中的所有包,并从manifest.json中提取信息
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
packs_dir: 包目录路径
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
list: 包配置列表
|
|
126
|
+
"""
|
|
127
|
+
packs_config = []
|
|
128
|
+
|
|
129
|
+
# 遍历目录下的所有子目录
|
|
130
|
+
for pack_name in os.listdir(packs_dir):
|
|
131
|
+
pack_path = os.path.join(packs_dir, pack_name)
|
|
132
|
+
|
|
133
|
+
# 只处理目录
|
|
134
|
+
if not os.path.isdir(pack_path):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# 查找manifest.json或pack_manifest.json
|
|
138
|
+
manifest_path = os.path.join(pack_path, "manifest.json")
|
|
139
|
+
if not os.path.exists(manifest_path):
|
|
140
|
+
manifest_path = os.path.join(pack_path, "pack_manifest.json")
|
|
141
|
+
if not os.path.exists(manifest_path):
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
146
|
+
manifest = json.load(f)
|
|
147
|
+
|
|
148
|
+
# 提取UUID和版本信息
|
|
149
|
+
if 'header' in manifest:
|
|
150
|
+
pack_id = manifest['header'].get('uuid')
|
|
151
|
+
version = manifest['header'].get('version', [0, 0, 1])
|
|
152
|
+
|
|
153
|
+
# 确保版本是列表格式
|
|
154
|
+
if isinstance(version, list):
|
|
155
|
+
version_array = version
|
|
156
|
+
elif isinstance(version, dict) and 'major' in version and 'minor' in version and 'patch' in version:
|
|
157
|
+
version_array = [version['major'], version['minor'], version['patch']]
|
|
158
|
+
else:
|
|
159
|
+
version_array = [0, 0, 1] # 默认版本
|
|
160
|
+
|
|
161
|
+
if pack_id:
|
|
162
|
+
pack_config = {
|
|
163
|
+
"pack_id": pack_id,
|
|
164
|
+
"type": "Addon",
|
|
165
|
+
"version": version_array
|
|
166
|
+
}
|
|
167
|
+
packs_config.append(pack_config)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
click.echo(f"⚠️ 读取包配置失败: {pack_name} - {str(e)}", style="yellow")
|
|
170
|
+
|
|
171
|
+
return packs_config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
构建模块包,提供项目构建、文件处理和监控功能
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .project_builder import AddonProjectBuilder
|
|
7
|
+
from .dependency_manager import DependencyManager
|
|
8
|
+
|
|
9
|
+
__all__ = ['AddonProjectBuilder', 'ProjectWatcher', 'FileWatcher', 'DependencyManager']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
# Offline assembly does not need a filesystem observer. Keep the public
|
|
14
|
+
# watcher exports, loading their platform dependency only when requested.
|
|
15
|
+
if name in ('ProjectWatcher', 'FileWatcher'):
|
|
16
|
+
from . import watcher
|
|
17
|
+
value = getattr(watcher, name)
|
|
18
|
+
globals()[name] = value
|
|
19
|
+
return value
|
|
20
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""完整构建和增量更新共用相同的文件合并规则。"""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..dependencies import DependencyError, canonical_path
|
|
7
|
+
from .AddonsPack import MANIFEST_FILES
|
|
8
|
+
from .file_merge import try_merge_file
|
|
9
|
+
from ..code_libraries import library_source
|
|
10
|
+
|
|
11
|
+
MERGED_JSON = {'blocks.json', 'terrain_texture.json', 'item_texture.json', 'sounds.json',
|
|
12
|
+
'sound_definitions.json', 'animations.json', 'animation_controllers.json',
|
|
13
|
+
'entity_models.json', 'render_controllers.json', 'materials.json',
|
|
14
|
+
'attachables.json', 'particle_effects.json', '_ui_defs.json'}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_within(path, parent):
|
|
18
|
+
try:
|
|
19
|
+
return os.path.commonpath([canonical_path(path), canonical_path(parent)]) == canonical_path(parent)
|
|
20
|
+
except ValueError:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_target(target, root, packs):
|
|
25
|
+
if not target:
|
|
26
|
+
raise DependencyError('未指定构建输出目录')
|
|
27
|
+
# 根目录下的独立 build 允许存在,但不能覆盖任意源包或依赖项目。
|
|
28
|
+
if is_within(root, target):
|
|
29
|
+
raise DependencyError(f'构建输出会覆盖主项目: {target}')
|
|
30
|
+
for pack in packs:
|
|
31
|
+
protected = [pack.path] if not pack.is_origin else [pack.behavior_pack_dir, pack.resource_pack_dir]
|
|
32
|
+
for source in filter(None, protected):
|
|
33
|
+
if is_within(source, target) or is_within(target, source):
|
|
34
|
+
raise DependencyError(f'构建输出与源目录重叠: {target} <-> {source}')
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pack_files(pack, kind):
|
|
38
|
+
if kind == 'behavior':
|
|
39
|
+
for library in getattr(pack, 'code_libraries', []):
|
|
40
|
+
for file in sorted(library['source'].rglob('*')):
|
|
41
|
+
if file.is_file():
|
|
42
|
+
yield library['target'] + '/' + file.relative_to(library['source']).as_posix()
|
|
43
|
+
folder = getattr(pack, kind + '_pack_dir')
|
|
44
|
+
if not folder or not os.path.isdir(folder):
|
|
45
|
+
return
|
|
46
|
+
for current, dirs, files in os.walk(folder):
|
|
47
|
+
dirs[:] = sorted(d for d in dirs if not pack.should_exclude(os.path.join(current, d)))
|
|
48
|
+
for name in sorted(files):
|
|
49
|
+
source = Path(current) / name
|
|
50
|
+
if name in MANIFEST_FILES or not pack.should_exclude(str(source)):
|
|
51
|
+
yield source.relative_to(folder).as_posix()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def rebuild_file(packs, kind, relative, destination):
|
|
55
|
+
"""按依赖优先序重建单个产物;所有来源删除后同步删除产物。"""
|
|
56
|
+
destination = Path(destination)
|
|
57
|
+
if destination.is_file() or destination.is_symlink():
|
|
58
|
+
destination.unlink()
|
|
59
|
+
sources = []
|
|
60
|
+
for pack in packs:
|
|
61
|
+
folder = getattr(pack, kind + '_pack_dir')
|
|
62
|
+
if not folder:
|
|
63
|
+
continue
|
|
64
|
+
mounted = library_source(pack, kind, relative)
|
|
65
|
+
source = mounted or Path(folder) / relative
|
|
66
|
+
if source.is_file() and (mounted or source.name in MANIFEST_FILES or not pack.should_exclude(str(source))):
|
|
67
|
+
sources.append((pack, source))
|
|
68
|
+
if relative in MANIFEST_FILES:
|
|
69
|
+
# 优先保留主包 manifest;主包缺少该包类型时使用最后一个依赖的 manifest。
|
|
70
|
+
manifest_owner = next((pack for pack in reversed(packs)
|
|
71
|
+
if getattr(pack, kind + '_pack_dir') and any(
|
|
72
|
+
(Path(getattr(pack, kind + '_pack_dir')) / name).is_file()
|
|
73
|
+
for name in MANIFEST_FILES)), None)
|
|
74
|
+
sources = [(pack, source) for pack, source in sources if pack is manifest_owner]
|
|
75
|
+
for pack, source in sources:
|
|
76
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
if destination.exists() and (source.name in MERGED_JSON or source.suffix == '.lang'):
|
|
78
|
+
success, error = try_merge_file(str(source), str(destination))
|
|
79
|
+
if not success:
|
|
80
|
+
raise DependencyError(error)
|
|
81
|
+
elif source.suffix == '.py':
|
|
82
|
+
pack._copy_with_encoding_check(str(source), str(destination))
|
|
83
|
+
else:
|
|
84
|
+
shutil.copy2(source, destination)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def assemble_addon(packs, target_pack):
|
|
88
|
+
for kind in ('behavior', 'resource'):
|
|
89
|
+
target = getattr(target_pack, kind + '_pack_dir')
|
|
90
|
+
# Empty directories are part of the pack layout too (e.g. entities/).
|
|
91
|
+
for pack in packs:
|
|
92
|
+
folder = getattr(pack, kind + '_pack_dir')
|
|
93
|
+
if not folder or not os.path.isdir(folder):
|
|
94
|
+
continue
|
|
95
|
+
for current, dirs, _ in os.walk(folder):
|
|
96
|
+
dirs[:] = sorted(d for d in dirs if not pack.should_exclude(os.path.join(current, d)))
|
|
97
|
+
(Path(target) / Path(current).relative_to(folder)).mkdir(parents=True, exist_ok=True)
|
|
98
|
+
paths = {relative for pack in packs for relative in pack_files(pack, kind)}
|
|
99
|
+
for relative in sorted(paths):
|
|
100
|
+
rebuild_file(packs, kind, relative, Path(target) / relative)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""将 Python 包与本地目录解析为统一的 Addon 依赖图。"""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from importlib import metadata
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
from urllib.request import url2pathname
|
|
10
|
+
|
|
11
|
+
from packaging.requirements import Requirement, InvalidRequirement
|
|
12
|
+
from packaging.utils import canonicalize_name
|
|
13
|
+
from ..dependencies import (DependencyError, DependencyStatus, addon_directories, canonical_path,
|
|
14
|
+
declarations, read_project, resolve_path, validate_game_files)
|
|
15
|
+
from .AddonsPack import AddonsPack
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _decode_direct_url(direct_url_path):
|
|
19
|
+
return _local_url(json.loads(Path(direct_url_path).read_text(encoding='utf-8')).get('url', ''))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _local_url(url):
|
|
23
|
+
parsed = urlsplit(url)
|
|
24
|
+
if parsed.scheme != 'file':
|
|
25
|
+
return None
|
|
26
|
+
path = url2pathname(parsed.path)
|
|
27
|
+
if parsed.netloc and parsed.netloc != 'localhost':
|
|
28
|
+
path = '//' + parsed.netloc + path
|
|
29
|
+
return str(Path(path).resolve())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DependencyNode:
|
|
33
|
+
def __init__(self, name, addon_pack, parent=None, *, source=None):
|
|
34
|
+
self.name, self.addon_pack, self.parent = name, addon_pack, parent
|
|
35
|
+
self.children = []
|
|
36
|
+
self.source = source or {'kind': 'local', 'path': str(Path(addon_pack.path).resolve())}
|
|
37
|
+
|
|
38
|
+
@cached_property
|
|
39
|
+
def source_root(self):
|
|
40
|
+
if self.source['kind'] == 'git':
|
|
41
|
+
from ..git_cache import read_snapshot
|
|
42
|
+
return read_snapshot(self.source['git'], self.source['rev'], self.source.get('source_sha256'))
|
|
43
|
+
return Path(self.addon_pack.path).resolve()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def descriptor_root(self):
|
|
47
|
+
root = self.source_root
|
|
48
|
+
if self.source['kind'] == 'git':
|
|
49
|
+
path = (root / self.source['subdir']).resolve()
|
|
50
|
+
if not path.is_relative_to(root.resolve()):
|
|
51
|
+
raise DependencyError('Git描述根越界')
|
|
52
|
+
from ..source_files import long_path
|
|
53
|
+
return long_path(path)
|
|
54
|
+
return root
|
|
55
|
+
|
|
56
|
+
def add_child(self, node):
|
|
57
|
+
if node not in self.children:
|
|
58
|
+
self.children.append(node)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DependencyManager:
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self.dependency_map = {}
|
|
64
|
+
self.root_node = None
|
|
65
|
+
self.warnings = []
|
|
66
|
+
self.errors = []
|
|
67
|
+
self.statuses = {}
|
|
68
|
+
self._nodes, self._active, self._ordered = {}, [], []
|
|
69
|
+
self._git_locks = {}
|
|
70
|
+
self._git_sources = {}
|
|
71
|
+
self.collect_errors = False
|
|
72
|
+
self.has_git_projects = False
|
|
73
|
+
|
|
74
|
+
def find_dependency_path(self, package_name):
|
|
75
|
+
try:
|
|
76
|
+
dist = metadata.distribution(Requirement(package_name).name)
|
|
77
|
+
except (metadata.PackageNotFoundError, InvalidRequirement):
|
|
78
|
+
return None
|
|
79
|
+
direct = dist.read_text('direct_url.json')
|
|
80
|
+
return _local_url(json.loads(direct).get('url', '')) if direct else None
|
|
81
|
+
|
|
82
|
+
def build_dependency_tree(self, project_name, project_path, dependencies=None, *, config=None, allow_missing=False, git_lock=None, collect_errors=False):
|
|
83
|
+
self.__init__()
|
|
84
|
+
self.collect_errors = collect_errors
|
|
85
|
+
project_path = str(Path(project_path).resolve())
|
|
86
|
+
if git_lock is not None:
|
|
87
|
+
self._git_locks[project_path] = git_lock
|
|
88
|
+
config = read_project(project_path) if config is None else config
|
|
89
|
+
if dependencies is not None:
|
|
90
|
+
config.setdefault('project', {})['dependencies'] = dependencies
|
|
91
|
+
root_id = re.sub(r'[^\w.-]', '_', project_name)[:60] + '_' + hashlib.sha256(canonical_path(project_path).encode('utf-8')).hexdigest()[:12]
|
|
92
|
+
root = DependencyNode(project_name, AddonsPack(root_id, project_path, is_origin=True))
|
|
93
|
+
folders = [root.addon_pack.behavior_pack_dir, root.addon_pack.resource_pack_dir]
|
|
94
|
+
if config.get('tool', {}).get('mcpywrap', {}).get('project_type') == 'map':
|
|
95
|
+
folders = [Path(project_path) / name for name in ('behavior_packs', 'resource_packs')]
|
|
96
|
+
validate_game_files(project_path, folders)
|
|
97
|
+
self.root_node = root
|
|
98
|
+
key = canonical_path(project_path)
|
|
99
|
+
self._nodes[key] = root
|
|
100
|
+
self._active.append(key)
|
|
101
|
+
self._process(root, config)
|
|
102
|
+
self._active.pop()
|
|
103
|
+
self.dependency_map = {canonical_path(n.addon_pack.path): n.addon_pack for n in self._ordered}
|
|
104
|
+
if self.errors and not (allow_missing or collect_errors):
|
|
105
|
+
raise DependencyError('\n'.join(self.errors))
|
|
106
|
+
return root
|
|
107
|
+
|
|
108
|
+
def _process(self, parent, config):
|
|
109
|
+
for entry in declarations(config):
|
|
110
|
+
source = parent.addon_pack.path
|
|
111
|
+
try:
|
|
112
|
+
if entry.kind == 'local':
|
|
113
|
+
path = str(resolve_path(source, entry.value))
|
|
114
|
+
self._visit(parent, path, Path(path).name)
|
|
115
|
+
self._status(source, entry, 'addon')
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
requirement = Requirement(entry.value)
|
|
119
|
+
except InvalidRequirement as exc:
|
|
120
|
+
raise DependencyError(f'无效的 Python 包声明: {entry.value}') from exc
|
|
121
|
+
if requirement.marker and not requirement.marker.evaluate():
|
|
122
|
+
self._status(source, entry, 'inactive', '当前工具 Python 环境不满足环境标记,未启用;不代表游戏兼容性。')
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
dist = metadata.distribution(requirement.name)
|
|
126
|
+
except metadata.PackageNotFoundError:
|
|
127
|
+
self._missing(source, entry.value, '尚未安装')
|
|
128
|
+
continue
|
|
129
|
+
if requirement.specifier and not requirement.specifier.contains(dist.version, prereleases=True):
|
|
130
|
+
self._missing(source, entry.value, f'已安装 {dist.version},不满足版本约束')
|
|
131
|
+
continue
|
|
132
|
+
path = self.find_dependency_path(entry.value)
|
|
133
|
+
if not path:
|
|
134
|
+
self._development_only(source, entry)
|
|
135
|
+
continue
|
|
136
|
+
direct = dist.read_text('direct_url.json')
|
|
137
|
+
if not Path(path).is_dir() and direct and 'dir_info' in json.loads(direct):
|
|
138
|
+
self._missing(source, entry.value, f'安装来源目录不存在: {path}')
|
|
139
|
+
continue
|
|
140
|
+
child_config = read_project(path)
|
|
141
|
+
has_mcpy = 'mcpywrap' in child_config.get('tool', {})
|
|
142
|
+
looks_like_addon = Path(path).is_dir() and any(
|
|
143
|
+
p.is_dir() and p.name.startswith(('behavior_pack', 'BehaviorPack', 'resource_pack', 'ResourcePack'))
|
|
144
|
+
for p in Path(path).iterdir())
|
|
145
|
+
if has_mcpy or looks_like_addon:
|
|
146
|
+
self._visit(parent, path, canonicalize_name(requirement.name), source_kind='package')
|
|
147
|
+
self._status(source, entry, 'addon')
|
|
148
|
+
else:
|
|
149
|
+
self._development_only(source, entry)
|
|
150
|
+
except (DependencyError, OSError, ValueError) as exc:
|
|
151
|
+
message = f'来源 {source}\n依赖 {entry.value!r}: {exc}'
|
|
152
|
+
if not self.collect_errors:
|
|
153
|
+
raise DependencyError(message) from exc
|
|
154
|
+
self.errors.append(message)
|
|
155
|
+
from ..git_projects import resolve_projects
|
|
156
|
+
try:
|
|
157
|
+
projects = resolve_projects(parent.addon_pack.path, config,
|
|
158
|
+
self._git_locks.get(str(Path(parent.addon_pack.path).resolve())),
|
|
159
|
+
source_records=self._git_sources)
|
|
160
|
+
for entry, path, info in projects:
|
|
161
|
+
self.has_git_projects = True
|
|
162
|
+
self._visit(parent, str(path), entry['name'])
|
|
163
|
+
except (DependencyError, OSError, ValueError) as exc:
|
|
164
|
+
if not self.collect_errors:
|
|
165
|
+
raise
|
|
166
|
+
self.errors.append(str(exc))
|
|
167
|
+
|
|
168
|
+
def _missing(self, source, value, reason):
|
|
169
|
+
from ..dependencies import DependencyDeclaration
|
|
170
|
+
message = f'{source}: {value} {reason};请显式执行 mcpy add "{value}"'
|
|
171
|
+
self.errors.append(message)
|
|
172
|
+
self.warnings.append(message)
|
|
173
|
+
self._status(source, DependencyDeclaration('package', value), 'unavailable', message)
|
|
174
|
+
|
|
175
|
+
def _status(self, source, entry, state, message=''):
|
|
176
|
+
self.statuses[(canonical_path(source), entry.kind, entry.value)] = DependencyStatus(state, message)
|
|
177
|
+
|
|
178
|
+
def status_for(self, source, entry):
|
|
179
|
+
return self.statuses[(canonical_path(source), entry.kind, entry.value)]
|
|
180
|
+
|
|
181
|
+
def _development_only(self, source, entry):
|
|
182
|
+
message = (f'来源 {source},依赖 {entry.value!r}: 未识别到可组装内容;'
|
|
183
|
+
'仅安装于工具环境,不会被打包或供游戏导入。')
|
|
184
|
+
self.warnings.append(message)
|
|
185
|
+
self._status(source, entry, 'development_only', message)
|
|
186
|
+
|
|
187
|
+
def _visit(self, parent, path, name, source_kind='local'):
|
|
188
|
+
key = canonical_path(path)
|
|
189
|
+
if key in self._active:
|
|
190
|
+
raise DependencyError('循环依赖: ' + ' -> '.join(self._active + [key]))
|
|
191
|
+
if key in self._nodes:
|
|
192
|
+
if source_kind == 'local' and self._nodes[key].source['kind'] == 'package':
|
|
193
|
+
self._nodes[key].source['kind'] = 'local'
|
|
194
|
+
parent.add_child(self._nodes[key])
|
|
195
|
+
return
|
|
196
|
+
config = read_project(path)
|
|
197
|
+
if config.get('tool', {}).get('mcpywrap', {}).get('project_type', 'addon') != 'addon':
|
|
198
|
+
raise DependencyError(f'仅支持 Addon 目录依赖: {path}')
|
|
199
|
+
folders = addon_directories(path)
|
|
200
|
+
validate_game_files(path, folders.values())
|
|
201
|
+
suffix = hashlib.sha256(key.encode('utf-8')).hexdigest()[:12]
|
|
202
|
+
safe_name = re.sub(r'[^\w.-]', '_', name)[:60] or 'addon'
|
|
203
|
+
addon = AddonsPack(f'{safe_name}_{suffix}', path)
|
|
204
|
+
addon.behavior_pack_dir, addon.resource_pack_dir = folders.get('behavior'), folders.get('resource')
|
|
205
|
+
addon.code_target = config.get('tool', {}).get('mcpywrap', {}).get('_registered_code_target')
|
|
206
|
+
record = self._git_sources.get(key)
|
|
207
|
+
source = ({'kind': 'git', 'git': record['source']['git'], 'rev': record['source']['rev'],
|
|
208
|
+
'subdir': record['source'].get('subdir', '.'), 'source_sha256': record.get('source_sha256')}
|
|
209
|
+
if record else {'kind': source_kind, 'path': str(Path(path).resolve())})
|
|
210
|
+
node = DependencyNode(name, addon, parent, source=source)
|
|
211
|
+
self._nodes[key] = node
|
|
212
|
+
parent.add_child(node)
|
|
213
|
+
self._active.append(key)
|
|
214
|
+
config = read_project(path)
|
|
215
|
+
try:
|
|
216
|
+
if 'mcpywrap' in config.get('tool', {}):
|
|
217
|
+
self._process(node, config)
|
|
218
|
+
finally:
|
|
219
|
+
self._active.pop()
|
|
220
|
+
self._ordered.append(node)
|
|
221
|
+
|
|
222
|
+
def get_all_dependencies(self):
|
|
223
|
+
return self.dependency_map
|
|
224
|
+
|
|
225
|
+
def get_dependency_tree(self):
|
|
226
|
+
return self.root_node
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def find_all_mcpywrap_packages():
|
|
230
|
+
packages = []
|
|
231
|
+
for dist in metadata.distributions():
|
|
232
|
+
try:
|
|
233
|
+
direct = dist.read_text('direct_url.json')
|
|
234
|
+
path = _local_url(json.loads(direct).get('url', '')) if direct else None
|
|
235
|
+
if path and 'mcpywrap' in read_project(path).get('tool', {}):
|
|
236
|
+
packages.append(dist.metadata['Name'])
|
|
237
|
+
except (ValueError, OSError):
|
|
238
|
+
continue
|
|
239
|
+
return sorted(set(packages))
|