mmccli 0.1.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.
mmccli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .app import app
mmccli/add.py ADDED
@@ -0,0 +1,327 @@
1
+ # mmccli.minecraft.add
2
+ import json
3
+ import os
4
+ import ssl
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import certifi
10
+ import requests
11
+ from pypdl import Pypdl
12
+ from pypdl.utils import EFuture, FileValidator
13
+ from rich.progress import Progress
14
+
15
+ from mmccli.cache import (
16
+ get_assets_cache_file_path,
17
+ get_forge_installer_cache_file_path,
18
+ get_library_cache_file_path,
19
+ get_version_jar_cache_file_path,
20
+ link_or_move,
21
+ )
22
+ from mmccli.forge import ForgeInstallerVersionInfo
23
+ from mmccli.forge.model import ForgeInstallerInstallProfileJson, ForgeInstallerVersionJson
24
+ from mmccli.log import RequestStatusError, console, out, warn
25
+ from mmccli.minecraft import (
26
+ McGameAssetIndexJson,
27
+ McGameVersionJson,
28
+ McGameVersionJson_FileModel,
29
+ McGameVersionJson_LibrariesItem,
30
+ McLauncherLaunchProfilesJson,
31
+ McLauncherLaunchProfilesJson_ProfilesItem,
32
+ validate_assets,
33
+ validate_libraries,
34
+ )
35
+
36
+ ssl_context = ssl.create_default_context(cafile=certifi.where())
37
+
38
+ COPY_DICT = dict[str | Path, str | Path]
39
+
40
+
41
+ def load_mc_version_json(version_json_url: str) -> McGameVersionJson:
42
+ response = requests.get(version_json_url)
43
+ out(version_json_url)
44
+ data = response.json()
45
+ if not response.ok:
46
+ raise RequestStatusError(f"获取游戏版本 version.json 时请求失败 | {version_json_url=}")
47
+ return McGameVersionJson.model_validate(data)
48
+
49
+
50
+ def load_assetsIndex_json(assetIndex_json_url: str) -> McGameAssetIndexJson:
51
+ response = requests.get(assetIndex_json_url)
52
+ out(assetIndex_json_url)
53
+ data = response.json()
54
+ if not response.ok:
55
+ raise RequestStatusError(f"获取游戏资源索引 assetIndex.json 时请求失败 | {assetIndex_json_url=}")
56
+ return McGameAssetIndexJson.model_validate(data)
57
+
58
+
59
+ def download_libraries(
60
+ libraries: list[McGameVersionJson_LibrariesItem],
61
+ dl: Pypdl,
62
+ libraries_from: Literal["minecraft", "forge", "fabric"],
63
+ ):
64
+ if libraries_from == "minecraft":
65
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_LIBRARIES_URL", None)
66
+ template_url_prefix = "https://libraries.minecraft.net/"
67
+ elif libraries_from == "forge":
68
+ base_url = os.environ.get("MCLI_FORGE_DOWNLOAD_LIBRARIES_URL", None)
69
+ template_url_prefix = "https://maven.minecraftforge.net/"
70
+ elif libraries_from == "fabric":
71
+ base_url = os.environ.get("MCLI_FABRIC_DOWNLOAD_LIBRARIES_URL", None)
72
+ template_url_prefix = "https://maven.fabricmc.net/"
73
+ else:
74
+ raise RuntimeError(f"Unexpected value {libraries_from=}")
75
+
76
+ # 并非所有库文件都要下载,此处存放需要下载的库文件
77
+ download_list: list[McGameVersionJson_LibrariesItem] = []
78
+
79
+ for item in libraries:
80
+ if item.rules is None: # 没有 rules 的 library
81
+ download_list.append(item)
82
+ continue
83
+
84
+ for rule in item.rules:
85
+ if rule.action == "allow" and rule.os and rule.os.name == "windows":
86
+ download_list.append(item)
87
+
88
+ # 拼装任务列表,批量下载
89
+ tasks = []
90
+ copy_dict: COPY_DICT = {}
91
+ total_size = 0
92
+ for item in download_list:
93
+ item: McGameVersionJson_LibrariesItem # type: ignore[no-redef]
94
+ if item.downloads.artifact.url == "": # 为什么会存在 library 的 url 为空呢?
95
+ continue # 原来这个玩意是不需要手动下载的呀!
96
+ file_path = Path.cwd() / ".minecraft" / "libraries" / item.downloads.artifact.path # 游戏目录中的库文件路径
97
+ file_path.parent.mkdir(exist_ok=True, parents=True)
98
+ cache_file_path = get_library_cache_file_path(item.downloads.artifact.path) # 缓存中的库文件路径
99
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
100
+ copy_dict[file_path] = cache_file_path # 建立关联
101
+ if cache_file_path.exists():
102
+ continue # 信任缓存,跳过缓存命中
103
+ if item.downloads.artifact.size:
104
+ total_size += item.downloads.artifact.size
105
+ url = (
106
+ item.downloads.artifact.url.replace(template_url_prefix, base_url)
107
+ if base_url
108
+ else item.downloads.artifact.url
109
+ )
110
+ out(url)
111
+ tasks.append(
112
+ {
113
+ "url": url,
114
+ "file_path": str(cache_file_path),
115
+ "retries": 2,
116
+ "segments": 8 if item.downloads.artifact.size and item.downloads.artifact.size >= 2_000_000 else 2,
117
+ "mirrors": item.downloads.artifact.url if base_url else None,
118
+ }
119
+ )
120
+ out(f"待下载库文件数量 | {len(tasks)=}")
121
+
122
+ if len(tasks) > 0:
123
+ results = _multi_download_with_progress(tasks, total_size, dl)
124
+ validate_libraries(download_list, results)
125
+ out("链接或复制文件……")
126
+
127
+ for file_path, cache_file_path in copy_dict.items(): # type: ignore[assignment]
128
+ link_or_move(cache_file_path, file_path)
129
+ out("库文件已下载完成。")
130
+
131
+
132
+ def download_assets(assetIndex_data: McGameAssetIndexJson, dl: Pypdl):
133
+ # 拼装任务列表,批量下载
134
+ tasks = []
135
+ copy_dict: COPY_DICT = {}
136
+ total_size = 0
137
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_ASSETS_URL", None)
138
+ for item in assetIndex_data.objects.values():
139
+ file_path = Path.cwd() / ".minecraft" / "assets" / "objects" / item.hash[:2] / item.hash
140
+ file_path.parent.mkdir(exist_ok=True, parents=True)
141
+ cache_file_path = get_assets_cache_file_path(f"objects/{item.hash[:2]}/{item.hash}")
142
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
143
+ copy_dict[file_path] = cache_file_path
144
+ if cache_file_path.exists():
145
+ continue # 信任缓存,跳过缓存命中
146
+ total_size += item.size
147
+ path = item.hash[:2] + "/" + item.hash
148
+ url = (base_url if base_url else "https://resources.download.minecraft.net/") + path
149
+ out(url)
150
+ tasks.append(
151
+ {
152
+ "url": url,
153
+ "file_path": str(cache_file_path),
154
+ "retries": 2,
155
+ "segments": 4 if item.size >= 1_000_000 else 1,
156
+ "mirrors": ("https://resources.download.minecraft.net/" + path) if base_url else None,
157
+ }
158
+ )
159
+ out(f"待下载资源文件数量 | {len(tasks)=}")
160
+
161
+ if len(tasks) > 0:
162
+ results = _multi_download_with_progress(tasks, total_size, dl)
163
+ validate_assets(list(assetIndex_data.objects.values()), results)
164
+ out("链接或复制文件……")
165
+
166
+ for file_path, cache_file_path in copy_dict.items(): # type: ignore[assignment]
167
+ link_or_move(cache_file_path, file_path)
168
+ out("资源文件已下载完成。")
169
+
170
+
171
+ def download_mc_version_jar(version_id: str, clientItem: McGameVersionJson_FileModel, dl: Pypdl, name: str):
172
+ file_path = Path.cwd() / ".minecraft" / "versions" / name / (name + ".jar")
173
+ file_path.parent.mkdir(exist_ok=True, parents=True)
174
+ cache_file_path = get_version_jar_cache_file_path("client/" + version_id + ".jar")
175
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
176
+ base_url = os.environ.get("MCLI_MC_GAME_DOWNLOAD_VERSIONS_URL", None)
177
+ if not cache_file_path.exists(): # 信任缓存
178
+ result = _single_download_with_progress(
179
+ url=clientItem.url.replace("https://piston-data.mojang.com/", base_url) if base_url else clientItem.url,
180
+ mirrors=clientItem.url if base_url else None,
181
+ file_path=str(cache_file_path),
182
+ size=clientItem.size,
183
+ dl=dl,
184
+ )
185
+ if result[0][1].get_hash("sha1") != clientItem.sha1:
186
+ warn("下载的版本游戏主文件 version.jar 存在损坏。")
187
+ out("链接或复制文件……")
188
+
189
+ link_or_move(cache_file_path, file_path)
190
+ out("版本 jar 游戏主文件下载完成。")
191
+
192
+
193
+ def download_forge_installer_jar(info: ForgeInstallerVersionInfo, dl: Pypdl):
194
+ cache_file_path = get_forge_installer_cache_file_path(info.name)
195
+ cache_file_path.parent.mkdir(exist_ok=True, parents=True)
196
+ if not cache_file_path.exists(): # 信任缓存
197
+ result = _single_download_with_progress(
198
+ url=f"https://bmclapi2.bangbang93.com/forge/download/{info.build}",
199
+ file_path=str(cache_file_path),
200
+ dl=dl,
201
+ )
202
+ if result[0][1].get_hash("sha1") != info.hash:
203
+ warn("下载的 Forge 安装器文件存在损坏。")
204
+
205
+ return cache_file_path
206
+
207
+
208
+ def analysis_forge_installer_version_json(file_path: str):
209
+ from tempfile import TemporaryDirectory
210
+ from zipfile import ZipFile
211
+
212
+ with TemporaryDirectory() as tmp_dir:
213
+ tmp_path = Path(tmp_dir)
214
+ with ZipFile(file=file_path, mode="r") as z:
215
+ z.extractall(tmp_path / "forge-installer-extractall")
216
+
217
+ with open(file=tmp_path / "forge-installer-extractall" / "version.json") as f:
218
+ version_json = ForgeInstallerVersionJson.model_validate_json(f.read())
219
+
220
+ with open(file=tmp_path / "forge-installer-extractall" / "install_profile.json") as f:
221
+ install_profile_json = ForgeInstallerInstallProfileJson.model_validate_json(f.read())
222
+
223
+ out("已在临时目录解压并解析 Forge 安装器内的 version.json 和 install_profile.json")
224
+ return version_json, install_profile_json
225
+
226
+
227
+ def save_mc_version_json(version_json: McGameVersionJson, name: str):
228
+ file_path = Path.cwd() / ".minecraft" / "versions" / name / (name + ".json")
229
+ file_path.parent.mkdir(exist_ok=True, parents=True)
230
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
231
+ f.write(version_json.model_dump_json(exclude_unset=True, indent=4))
232
+ out("已保存版本 json 文件。")
233
+
234
+
235
+ def save_assetIndex_json(assetIndex_id: str, assetIndex_json: McGameAssetIndexJson):
236
+ file_path = Path.cwd() / ".minecraft" / "assets" / "indexes" / (assetIndex_id + ".json")
237
+ file_path.parent.mkdir(exist_ok=True, parents=True)
238
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
239
+ f.write(assetIndex_json.model_dump_json(exclude_unset=True, indent=4))
240
+ out("已保存资源文件索引 json 文件。")
241
+
242
+
243
+ def save_launcher_profiles_json(mc_version_json: McGameVersionJson):
244
+ from datetime import datetime
245
+ from uuid import uuid4
246
+
247
+ key = uuid4().hex
248
+ now = datetime.now()
249
+
250
+ # 已有启动器配置文件就写入,否则新建
251
+ file_path = Path.cwd() / ".minecraft" / "launcher_profiles.json"
252
+ file_path.parent.mkdir(exist_ok=True, parents=True)
253
+ profile = McLauncherLaunchProfilesJson_ProfilesItem(
254
+ created=now.isoformat(),
255
+ icon="Grass",
256
+ javaArgs="-Xms2G -Xmx4G -XX:+UseCompactObjectHeaders -XX:+AlwaysPreTouch -XX:+UseStringDeduplication -XX:+UseZGC",
257
+ lastUsed="1970-01-01T00:00:00.000Z",
258
+ lastVersionId=mc_version_json.id,
259
+ name=mc_version_json.id,
260
+ type="custom",
261
+ )
262
+ if file_path.exists():
263
+ with open(file=file_path, mode="r") as f:
264
+ launcher_profiles = McLauncherLaunchProfilesJson.model_validate_json(f.read())
265
+ launcher_profiles.profiles["key"] = profile
266
+ else:
267
+ launcher_profiles = McLauncherLaunchProfilesJson(
268
+ profiles={key: profile},
269
+ settings={},
270
+ version=6,
271
+ )
272
+ with open(file=file_path, mode="w", encoding="UTF-8") as f:
273
+ f.write(launcher_profiles.model_dump_json(indent=4))
274
+ out("已更新或新建启动器配置 json 文件。")
275
+
276
+
277
+ def _multi_download_with_progress(tasks: list[dict], total_size: int, dl: Pypdl) -> list[tuple[str, FileValidator]]:
278
+ future: EFuture = dl.start(
279
+ tasks=tasks,
280
+ block=False,
281
+ hash_algorithms="sha1",
282
+ display=False,
283
+ proxy=os.environ.get("HTTP_PROXY", None),
284
+ overwrite=False,
285
+ ssl=ssl_context,
286
+ )
287
+
288
+ if len(tasks) > 0:
289
+ with Progress(console=console, transient=True) as progress:
290
+ task_files = progress.add_task("[cyan]下载总文件", total=dl.total_task)
291
+ task_size = progress.add_task("[green]下载总空间", total=total_size)
292
+ while not dl.completed:
293
+ progress.update(task_files, completed=dl.completed_task)
294
+ progress.update(task_size, completed=dl.current_size)
295
+ time.sleep(0.5)
296
+
297
+ return future.result() # type: ignore[no-any-return]
298
+
299
+
300
+ def _single_download_with_progress(
301
+ url: str,
302
+ file_path: str,
303
+ dl: Pypdl,
304
+ mirrors: str | list[str] | None = None,
305
+ size: int | None = None,
306
+ ) -> list[tuple[str, FileValidator]]:
307
+ future: EFuture = dl.start(
308
+ url=url,
309
+ file_path=file_path,
310
+ mirrors=mirrors,
311
+ segments=8,
312
+ retries=2,
313
+ block=False,
314
+ overwrite=False,
315
+ hash_algorithms="sha1",
316
+ display=False,
317
+ proxy=os.environ.get("HTTP_PROXY", None),
318
+ ssl=ssl_context,
319
+ )
320
+
321
+ with Progress(console=console, transient=True) as progress:
322
+ task_size = progress.add_task("[green]下载空间", total=size)
323
+ while not dl.completed:
324
+ progress.update(task_size, completed=dl.current_size)
325
+ time.sleep(0.5)
326
+
327
+ return future.result() # type: ignore[no-any-return]
mmccli/app.py ADDED
@@ -0,0 +1,227 @@
1
+ # mmccli.app
2
+ from dotenv import load_dotenv
3
+
4
+ from mmccli.forge import rename_forge_version
5
+ from mmccli.path_dector import detect_minecraft_install_folder
6
+
7
+ load_dotenv()
8
+
9
+ import re
10
+ from typing import Annotated
11
+
12
+ import typer
13
+
14
+ from mmccli.log import err, info, out, useGlobalState, warn
15
+ from mmccli.minecraft import app_minecraft, load_mc_version_from_cache
16
+
17
+ app = typer.Typer(help="Minecraft Java Cli", name="MCLI")
18
+ app.add_typer(app_minecraft)
19
+
20
+ pattern = re.compile(r"To sign in, use a web browser to open the page (\S*) and enter the code (\S*) to authenticate.")
21
+
22
+
23
+ @app.command()
24
+ def login():
25
+ """
26
+ 登录微软账号,验证您的 Minecraft Java 版游戏所有权。
27
+
28
+ 由于未获得 Mojang 批准所以 TvT
29
+ """
30
+ err("正版登录尚不可用,因为 Mojang 没有批准 MCLI 访问其游戏服务 API。")
31
+ raise typer.Exit(code=100)
32
+
33
+ from mmccli.ms_oauth import (
34
+ login_for_minecraft_with_xsts,
35
+ login_for_xbox_live,
36
+ login_for_xsts,
37
+ login_with_microsoft_oauth,
38
+ wait_for_access_token,
39
+ )
40
+
41
+ out("正在创建 Device Code 以登录微软账号……")
42
+
43
+ flow = login_with_microsoft_oauth()
44
+ validateInfo: tuple[str, str] = pattern.findall(flow["message"])[0]
45
+ info(f"请在浏览器中打开页面 {validateInfo[0]},并输入代码 {validateInfo[1]} 完成登录。")
46
+
47
+ ms_data = wait_for_access_token(flow) # 阻塞直到用户授权
48
+ out("已完成微软账号登录,开始 Xbox Live 验证……")
49
+
50
+ xbl_data = login_for_xbox_live(ms_data.access_token)
51
+ out("已完成 Xbox Live 验证,开始获取 XSTS 令牌。")
52
+
53
+ xsts_data = login_for_xsts(xbl_data.Token)
54
+ out("已获取 XSTS 令牌,开始获取 Minecraft 令牌。")
55
+
56
+ mc_data = login_for_minecraft_with_xsts(xsts_data.DisplayClaims.xui[0].uhs, xsts_data.Token)
57
+ out("已获取 Minecraft 令牌,开始获取玩家信息。")
58
+
59
+
60
+ @app.command()
61
+ def add(
62
+ mc_version_id: Annotated[
63
+ str, typer.Option("--minecraft", "-mc", help="Minecraft 本体版本,或用 latest-release 和 latest-snapshot")
64
+ ],
65
+ forge_version_promo_or_id: Annotated[
66
+ str | None,
67
+ typer.Option("--forge", "-fo", help="Forge 加载器版本,或用 latest 和 recommended,不安装 forge 请省略"),
68
+ ] = None,
69
+ fabric_version_promo_or_id: Annotated[
70
+ str | None,
71
+ typer.Option("--fabric", "-fa", help="Fabric 加载器版本,或用 latest,不安装 fabric 请省略"),
72
+ ] = None,
73
+ name: Annotated[str | None, typer.Option("--name", "-n", help="版本的自定义名称")] = None,
74
+ ):
75
+ """
76
+ 添加一个游戏版本并立刻开始构建。
77
+ """
78
+ if detect_minecraft_install_folder():
79
+ out(f"{useGlobalState.MINECRAFT=}")
80
+ else:
81
+ raise typer.Exit(code=418)
82
+
83
+ forge_version_info = None
84
+ fabric_version_info = None
85
+ if forge_version_promo_or_id and fabric_version_promo_or_id:
86
+ err("无法同时安装 Forge 和 Fabric 加载器。")
87
+ raise typer.Exit(code=500)
88
+
89
+ info("正在查找 Minecraft 版本信息……")
90
+ mc_version = load_mc_version_from_cache(mc_version_id, True) # latest-release 和 latest-snapshot 在此方法内处理
91
+ if not mc_version:
92
+ err(f"未找到游戏版本 {mc_version_id},请检查。")
93
+ raise typer.Exit(code=404)
94
+
95
+ if forge_version_promo_or_id:
96
+ from mmccli.forge import load_forge_version_from_cache
97
+
98
+ info("正在查找 Forge 版本信息……")
99
+ forge_version_info = load_forge_version_from_cache(mc_version.id, forge_version_promo_or_id, True)
100
+ if not forge_version_info:
101
+ err(f"未找到支持 {mc_version.id} 的 Forge 版本 {forge_version_promo_or_id},请检查。")
102
+ raise typer.Exit(code=404)
103
+ info(f"已确认 Forge 版本 {forge_version_info.name}")
104
+
105
+ if fabric_version_promo_or_id:
106
+ from mmccli.fabric import load_fabric_versions
107
+
108
+ info("正在查找 Fabric 版本信息……")
109
+ fabric_version_info = load_fabric_versions(mc_version.id, fabric_version_promo_or_id)
110
+ if not fabric_version_info:
111
+ err(f"未找到 Fabric 版本 {fabric_version_promo_or_id},请检查。")
112
+ raise typer.Exit(code=404)
113
+ info(f"已确认 Fabric 版本 {fabric_version_info.loader.version}")
114
+
115
+ if not name:
116
+ name = mc_version.id
117
+ if fabric_version_info:
118
+ name += "-fabric-" + fabric_version_info.loader.version
119
+
120
+ from mmccli.add import (
121
+ analysis_forge_installer_version_json,
122
+ download_assets,
123
+ download_forge_installer_jar,
124
+ download_libraries,
125
+ download_mc_version_jar,
126
+ load_assetsIndex_json,
127
+ load_mc_version_json,
128
+ save_assetIndex_json,
129
+ save_launcher_profiles_json,
130
+ save_mc_version_json,
131
+ )
132
+
133
+ version_json = load_mc_version_json(mc_version.url)
134
+ vanilla_mc_version_id = version_json.id
135
+ version_json.id = name # 将自定义版本名称覆盖到 version.json
136
+ info("已解析版本索引文件,开始下载库文件……")
137
+
138
+ from pypdl import Pypdl
139
+
140
+ dl = Pypdl(max_concurrent=48, allow_reuse=True)
141
+ download_libraries(version_json.libraries, dl, "minecraft")
142
+ info("库文件下载完成,开始解析资源索引文件……")
143
+
144
+ assetIndex_json = load_assetsIndex_json(version_json.assetIndex.url)
145
+ info("已解析资源索引文件,开始下载资源文件……")
146
+
147
+ download_assets(assetIndex_json, dl)
148
+ info("资源文件下载完成,开始下载游戏主文件……")
149
+
150
+ download_mc_version_jar(
151
+ vanilla_mc_version_id,
152
+ version_json.downloads["client"],
153
+ dl,
154
+ vanilla_mc_version_id if forge_version_info else name,
155
+ ) # 由于 Forge 安装器需要基于合法的原版游戏进行安装,所以要保留合法的原版游戏。
156
+ info("游戏主文件下载完成,保存资源索引……")
157
+
158
+ save_assetIndex_json(version_json.assets, assetIndex_json)
159
+ save_mc_version_json(version_json, name)
160
+ save_launcher_profiles_json(version_json)
161
+ info(f"已添加 Minecraft Java {version_json.id}")
162
+
163
+ if forge_version_info:
164
+ forge_installer_path = download_forge_installer_jar(forge_version_info, dl)
165
+ info(f"已下载 Forge 安装器 {forge_version_info.name},开始解析 Forge 版本信息……")
166
+
167
+ forge_version_json, forge_install_profile_json = analysis_forge_installer_version_json(forge_installer_path)
168
+ info("已解析 Forge 版本信息,开始下载 Forge 库文件……")
169
+
170
+ download_libraries(forge_version_json.libraries + forge_install_profile_json.libraries, dl, "forge")
171
+ info("Forge 库文件下载完成,开始安装 Forge……")
172
+
173
+ from mmccli.forge import install_forge_silently
174
+
175
+ if install_forge_silently(str(forge_installer_path), str(useGlobalState.MINECRAFT)):
176
+ rename_forge_version(vanilla_mc_version_id, forge_version_info.id, name)
177
+ info(f"{forge_version_info.name} 已为 Minecraft {version_json.id} 安装成功。(共安装两个实例)")
178
+ else:
179
+ warn(f"{forge_version_info.name} 安装失败,但游戏本体应当安装成功。")
180
+
181
+ if fabric_version_info:
182
+ from mmccli.fabric import build_version_json_libraries_item, update_mc_version_json_with_fabric
183
+
184
+ fabric_libraries = [
185
+ build_version_json_libraries_item(
186
+ name=library.name,
187
+ base_url=library.url if library.url else "https://libraries.mojang.com/",
188
+ size=library.size,
189
+ sha1=library.sha1,
190
+ )
191
+ for library in fabric_version_info.launcherMeta.libraries.common
192
+ + fabric_version_info.launcherMeta.libraries.client
193
+ ]
194
+ fabric_libraries.append(
195
+ build_version_json_libraries_item(
196
+ name=fabric_version_info.loader.maven,
197
+ base_url="https://maven.fabricmc.net/",
198
+ )
199
+ )
200
+ fabric_libraries.append(
201
+ build_version_json_libraries_item(
202
+ name=fabric_version_info.intermediary.maven,
203
+ base_url="https://maven.fabricmc.net/",
204
+ )
205
+ )
206
+ info("已解析 Fabric 版本信息,开始下载 Fabric 库文件……")
207
+
208
+ download_libraries(fabric_libraries, dl, "fabric")
209
+ info("Fabric 库文件下载完成,更新版本 json 文件中……")
210
+
211
+ save_mc_version_json(
212
+ update_mc_version_json_with_fabric(
213
+ version_json, fabric_libraries, fabric_version_info.launcherMeta.mainClass
214
+ ),
215
+ name,
216
+ )
217
+ info(f"Fabric loader {fabric_version_info.loader.version} 已为 Minecraft {version_json.id} 安装成功。")
218
+
219
+ dl.stop()
220
+ dl.shutdown()
221
+
222
+ if useGlobalState.HARDLINK_DOWNGRADE_TO_FULL_COPY:
223
+ warn("硬链接失败,已退化为全量复制文件。")
224
+
225
+
226
+ if __name__ == "__main__":
227
+ app()
@@ -0,0 +1,6 @@
1
+ https://github.com/bangbang93/forge-install-bootstrapper
2
+
3
+ 计划用于 Forge 的静默安装。
4
+
5
+ 作者:bangbang93
6
+ 版本:v0.3.0
@@ -0,0 +1,19 @@
1
+ # mmccli.cache
2
+
3
+ from ._lib import (
4
+ CACHE_ROOT_DIR,
5
+ get_assets_cache_file_path,
6
+ get_forge_installer_cache_file_path,
7
+ get_library_cache_file_path,
8
+ get_version_jar_cache_file_path,
9
+ link_or_move,
10
+ )
11
+
12
+ __all__ = [
13
+ "CACHE_ROOT_DIR",
14
+ "get_assets_cache_file_path",
15
+ "get_forge_installer_cache_file_path",
16
+ "get_library_cache_file_path",
17
+ "get_version_jar_cache_file_path",
18
+ "link_or_move",
19
+ ]
mmccli/cache/_lib.py ADDED
@@ -0,0 +1,47 @@
1
+ # mmccli.cache._lib
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from mmccli.log import useGlobalState
9
+
10
+ match platform.system():
11
+ case "Windows":
12
+ ENV_HOME_NAME = "USERPROFILE"
13
+ case _:
14
+ ENV_HOME_NAME = "HOME"
15
+ CACHE_ROOT_DIR = Path(os.environ.get(ENV_HOME_NAME, "")) / ".mmccli" / "cache"
16
+ CACHE_ROOT_DIR.mkdir(parents=True, exist_ok=True)
17
+
18
+
19
+ def get_library_cache_file_path(file_path: str | Path):
20
+ return CACHE_ROOT_DIR / "libraries" / file_path
21
+
22
+
23
+ def get_assets_cache_file_path(file_path: str | Path):
24
+ return CACHE_ROOT_DIR / "assets" / file_path
25
+
26
+
27
+ def get_version_jar_cache_file_path(file_path: str | Path):
28
+ return CACHE_ROOT_DIR / "versions" / file_path
29
+
30
+
31
+ def get_forge_installer_cache_file_path(file_path: str | Path):
32
+ return CACHE_ROOT_DIR / "forge-installer" / file_path
33
+
34
+
35
+ def link_or_move(cache_file_path: str | Path, file_path: str | Path):
36
+ if useGlobalState.HARDLINK_DOWNGRADE_TO_FULL_COPY:
37
+ shutil.copy(cache_file_path, file_path)
38
+ return
39
+
40
+ try:
41
+ if Path(file_path).exists():
42
+ os.remove(file_path)
43
+ os.link(cache_file_path, file_path)
44
+ except Exception:
45
+ if not useGlobalState.HARDLINK_DOWNGRADE_TO_FULL_COPY:
46
+ useGlobalState.HARDLINK_DOWNGRADE_TO_FULL_COPY = True
47
+ shutil.copy(cache_file_path, file_path)
@@ -0,0 +1,6 @@
1
+ # mmccli.fabric
2
+
3
+ from .install import build_version_json_libraries_item, update_mc_version_json_with_fabric
4
+ from .versions import load_fabric_versions
5
+
6
+ __all__ = ["build_version_json_libraries_item", "load_fabric_versions", "update_mc_version_json_with_fabric"]
@@ -0,0 +1,43 @@
1
+ # mmccli.fabric.install
2
+ import os
3
+
4
+ from mmccli.fabric.model import FabricMetaVersions_LoaderItem_LauncherMeta_MainClass
5
+ from mmccli.minecraft import (
6
+ McGameVersionJson,
7
+ McGameVersionJson_LibrariesItem,
8
+ McGameVersionJson_LibrariesItemDownloads,
9
+ McGameVersionJson_LibrariesItemDownloadsArtifact,
10
+ )
11
+
12
+
13
+ def build_version_json_libraries_item(name: str, base_url: str, size: int | None = None, sha1: str | None = None):
14
+ """
15
+ 产出依赖库结构中,path 是根据 name 拼接得到的,url 是根据 path 拼接完整的,sha1 是可能没有的。
16
+ :return:
17
+ """
18
+
19
+ parts: list[str] = name.split(":") # 长度为 3,形如 ["net.fabricmc", "fabric-loader", "0.16.14"]
20
+
21
+ path = "/".join(parts[0].split(".")) + "/" + parts[1] + "/" + parts[2] + "/" + parts[1] + "-" + parts[2] + ".jar"
22
+
23
+ return McGameVersionJson_LibrariesItem(
24
+ name=name,
25
+ downloads=McGameVersionJson_LibrariesItemDownloads(
26
+ artifact=McGameVersionJson_LibrariesItemDownloadsArtifact(
27
+ path=path,
28
+ url=os.environ.get("MCLI_FABRIC_DOWNLOAD_LIBRARIES_URL", base_url) + path,
29
+ size=size,
30
+ sha1=sha1,
31
+ )
32
+ ),
33
+ )
34
+
35
+
36
+ def update_mc_version_json_with_fabric(
37
+ version_json: McGameVersionJson,
38
+ fabric_libraries: list[McGameVersionJson_LibrariesItem],
39
+ mainClass: FabricMetaVersions_LoaderItem_LauncherMeta_MainClass | str,
40
+ ):
41
+ version_json.libraries += fabric_libraries
42
+ version_json.mainClass = mainClass if isinstance(mainClass, str) else mainClass.client
43
+ return version_json