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 +1 -0
- mmccli/add.py +327 -0
- mmccli/app.py +227 -0
- mmccli/bin/forge-install-bootstrapper.jar +0 -0
- mmccli/bin/whats this.txt +6 -0
- mmccli/cache/__init__.py +19 -0
- mmccli/cache/_lib.py +47 -0
- mmccli/fabric/__init__.py +6 -0
- mmccli/fabric/install.py +43 -0
- mmccli/fabric/model.py +72 -0
- mmccli/fabric/versions.py +35 -0
- mmccli/forge/__init__.py +12 -0
- mmccli/forge/install.py +67 -0
- mmccli/forge/model.py +106 -0
- mmccli/forge/promos.py +146 -0
- mmccli/log/__init__.py +34 -0
- mmccli/log/error.py +4 -0
- mmccli/log/gateway.py +8 -0
- mmccli/log/logger.py +23 -0
- mmccli/log/state.py +12 -0
- mmccli/minecraft/__init__.py +42 -0
- mmccli/minecraft/_model.py +53 -0
- mmccli/minecraft/app.py +66 -0
- mmccli/minecraft/locate.py +0 -0
- mmccli/minecraft/manifests.py +85 -0
- mmccli/minecraft/model.py +89 -0
- mmccli/minecraft/validate.py +38 -0
- mmccli/ms_oauth/__init__.py +12 -0
- mmccli/ms_oauth/_model.py +9 -0
- mmccli/ms_oauth/error.py +4 -0
- mmccli/ms_oauth/login.py +96 -0
- mmccli/ms_oauth/model.py +31 -0
- mmccli/path_dector.py +28 -0
- mmccli-0.1.0.dist-info/METADATA +69 -0
- mmccli-0.1.0.dist-info/RECORD +37 -0
- mmccli-0.1.0.dist-info/WHEEL +4 -0
- mmccli-0.1.0.dist-info/entry_points.txt +3 -0
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()
|
|
Binary file
|
mmccli/cache/__init__.py
ADDED
|
@@ -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)
|
mmccli/fabric/install.py
ADDED
|
@@ -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
|