mmccli 0.1.0__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.
- mmccli-0.1.0/PKG-INFO +69 -0
- mmccli-0.1.0/README.md +51 -0
- mmccli-0.1.0/pyproject.toml +34 -0
- mmccli-0.1.0/pyproject.toml.orig +33 -0
- mmccli-0.1.0/src/mmccli/__init__.py +1 -0
- mmccli-0.1.0/src/mmccli/add.py +327 -0
- mmccli-0.1.0/src/mmccli/app.py +227 -0
- mmccli-0.1.0/src/mmccli/bin/forge-install-bootstrapper.jar +0 -0
- mmccli-0.1.0/src/mmccli/bin/whats this.txt +6 -0
- mmccli-0.1.0/src/mmccli/cache/__init__.py +19 -0
- mmccli-0.1.0/src/mmccli/cache/_lib.py +47 -0
- mmccli-0.1.0/src/mmccli/fabric/__init__.py +6 -0
- mmccli-0.1.0/src/mmccli/fabric/install.py +43 -0
- mmccli-0.1.0/src/mmccli/fabric/model.py +72 -0
- mmccli-0.1.0/src/mmccli/fabric/versions.py +35 -0
- mmccli-0.1.0/src/mmccli/forge/__init__.py +12 -0
- mmccli-0.1.0/src/mmccli/forge/install.py +67 -0
- mmccli-0.1.0/src/mmccli/forge/model.py +106 -0
- mmccli-0.1.0/src/mmccli/forge/promos.py +146 -0
- mmccli-0.1.0/src/mmccli/log/__init__.py +34 -0
- mmccli-0.1.0/src/mmccli/log/error.py +4 -0
- mmccli-0.1.0/src/mmccli/log/gateway.py +8 -0
- mmccli-0.1.0/src/mmccli/log/logger.py +23 -0
- mmccli-0.1.0/src/mmccli/log/state.py +12 -0
- mmccli-0.1.0/src/mmccli/minecraft/__init__.py +42 -0
- mmccli-0.1.0/src/mmccli/minecraft/_model.py +53 -0
- mmccli-0.1.0/src/mmccli/minecraft/app.py +66 -0
- mmccli-0.1.0/src/mmccli/minecraft/locate.py +0 -0
- mmccli-0.1.0/src/mmccli/minecraft/manifests.py +85 -0
- mmccli-0.1.0/src/mmccli/minecraft/model.py +89 -0
- mmccli-0.1.0/src/mmccli/minecraft/validate.py +38 -0
- mmccli-0.1.0/src/mmccli/ms_oauth/__init__.py +12 -0
- mmccli-0.1.0/src/mmccli/ms_oauth/_model.py +9 -0
- mmccli-0.1.0/src/mmccli/ms_oauth/error.py +4 -0
- mmccli-0.1.0/src/mmccli/ms_oauth/login.py +96 -0
- mmccli-0.1.0/src/mmccli/ms_oauth/model.py +31 -0
- mmccli-0.1.0/src/mmccli/path_dector.py +28 -0
mmccli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mmccli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Minecraft Java Edition Manager in Commandline.
|
|
5
|
+
Author: MangoFanFanw
|
|
6
|
+
Author-email: MangoFanFanw <mangofanfanw@icloud.com>
|
|
7
|
+
Requires-Dist: certifi>=2026.10.19
|
|
8
|
+
Requires-Dist: click>=8.5.0
|
|
9
|
+
Requires-Dist: dotenv>=0.9.9
|
|
10
|
+
Requires-Dist: msal>=1.39.0
|
|
11
|
+
Requires-Dist: pydantic>=2.14.1
|
|
12
|
+
Requires-Dist: pypdl>=1.5.7
|
|
13
|
+
Requires-Dist: requests>=2.34.2
|
|
14
|
+
Requires-Dist: rich>=15.0.0
|
|
15
|
+
Requires-Dist: typer>=0.27.3
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Minecraft \(Java Edition Manager in\) Commandline
|
|
20
|
+
|
|
21
|
+
> Minecraft Java 版命令行管理工具,简写为 MCLI,命令为 mmccli。
|
|
22
|
+
>
|
|
23
|
+
> ~因为 mcli 已经重名了……~
|
|
24
|
+
|
|
25
|
+
这是一个使用 Python 实现的命令行工具,用于管理 Minecraft Java 版的版本实例。
|
|
26
|
+
|
|
27
|
+
或者简单来说是一个下载器。
|
|
28
|
+
|
|
29
|
+
**MCLI 不是一款 Minecraft 启动器,你仍然需要一个启动器来启动本工具添加的版本。**
|
|
30
|
+
|
|
31
|
+
## 特色
|
|
32
|
+
|
|
33
|
+
MCLI 致力于实现类似于 *包管理器*的使用体验。
|
|
34
|
+
|
|
35
|
+
* 依赖库文件、资源文件等全局缓存,在游戏目录中硬链接。
|
|
36
|
+
* 多任务、多线程并发下载,提高下载速度(也许)。
|
|
37
|
+
* 可配置镜像源,提高下载速度(也许)。
|
|
38
|
+
|
|
39
|
+
并且不只是下载游戏本体,MCLI 还支持:
|
|
40
|
+
|
|
41
|
+
* 添加模组加载器。
|
|
42
|
+
* 已支持 1.13+ Forge、Fabric,启动器可识别与启动。
|
|
43
|
+
* 正在计划支持 NeoForge 和 Quilt(这个还在更新吗?)
|
|
44
|
+
|
|
45
|
+
未来 MCLI 还长期计划:
|
|
46
|
+
|
|
47
|
+
* 支持一行命令从 CurseForge 和/或 Modrinth 安装模组。
|
|
48
|
+
* 支持一行命令安装服务器。
|
|
49
|
+
* 使用更快速的方式重新实现下载功能。
|
|
50
|
+
|
|
51
|
+
## 使用
|
|
52
|
+
|
|
53
|
+
### 安装(或者不安装)
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
uvx mmccli <command>
|
|
57
|
+
|
|
58
|
+
# 或者安装为工具,然后直接使用
|
|
59
|
+
uv tool install mmccli
|
|
60
|
+
mmccli <command>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 添加游戏
|
|
64
|
+
|
|
65
|
+
```shell
|
|
66
|
+
mmccli add --minecraft 26.1
|
|
67
|
+
mmccli add --minecraft 26.2 --forge latest
|
|
68
|
+
mmccli add --minecraft 26.3 --name "原版 26.3"
|
|
69
|
+
```
|
mmccli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Minecraft \(Java Edition Manager in\) Commandline
|
|
2
|
+
|
|
3
|
+
> Minecraft Java 版命令行管理工具,简写为 MCLI,命令为 mmccli。
|
|
4
|
+
>
|
|
5
|
+
> ~因为 mcli 已经重名了……~
|
|
6
|
+
|
|
7
|
+
这是一个使用 Python 实现的命令行工具,用于管理 Minecraft Java 版的版本实例。
|
|
8
|
+
|
|
9
|
+
或者简单来说是一个下载器。
|
|
10
|
+
|
|
11
|
+
**MCLI 不是一款 Minecraft 启动器,你仍然需要一个启动器来启动本工具添加的版本。**
|
|
12
|
+
|
|
13
|
+
## 特色
|
|
14
|
+
|
|
15
|
+
MCLI 致力于实现类似于 *包管理器*的使用体验。
|
|
16
|
+
|
|
17
|
+
* 依赖库文件、资源文件等全局缓存,在游戏目录中硬链接。
|
|
18
|
+
* 多任务、多线程并发下载,提高下载速度(也许)。
|
|
19
|
+
* 可配置镜像源,提高下载速度(也许)。
|
|
20
|
+
|
|
21
|
+
并且不只是下载游戏本体,MCLI 还支持:
|
|
22
|
+
|
|
23
|
+
* 添加模组加载器。
|
|
24
|
+
* 已支持 1.13+ Forge、Fabric,启动器可识别与启动。
|
|
25
|
+
* 正在计划支持 NeoForge 和 Quilt(这个还在更新吗?)
|
|
26
|
+
|
|
27
|
+
未来 MCLI 还长期计划:
|
|
28
|
+
|
|
29
|
+
* 支持一行命令从 CurseForge 和/或 Modrinth 安装模组。
|
|
30
|
+
* 支持一行命令安装服务器。
|
|
31
|
+
* 使用更快速的方式重新实现下载功能。
|
|
32
|
+
|
|
33
|
+
## 使用
|
|
34
|
+
|
|
35
|
+
### 安装(或者不安装)
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
uvx mmccli <command>
|
|
39
|
+
|
|
40
|
+
# 或者安装为工具,然后直接使用
|
|
41
|
+
uv tool install mmccli
|
|
42
|
+
mmccli <command>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 添加游戏
|
|
46
|
+
|
|
47
|
+
```shell
|
|
48
|
+
mmccli add --minecraft 26.1
|
|
49
|
+
mmccli add --minecraft 26.2 --forge latest
|
|
50
|
+
mmccli add --minecraft 26.3 --name "原版 26.3"
|
|
51
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mmccli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Minecraft Java Edition Manager in Commandline."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"certifi>=2026.10.19",
|
|
9
|
+
"click>=8.5.0",
|
|
10
|
+
"dotenv>=0.9.9",
|
|
11
|
+
"msal>=1.39.0",
|
|
12
|
+
"pydantic>=2.14.1",
|
|
13
|
+
"pypdl>=1.5.7",
|
|
14
|
+
"requests>=2.34.2",
|
|
15
|
+
"rich>=15.0.0",
|
|
16
|
+
"typer>=0.27.3",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[project.authors]]
|
|
20
|
+
name = "MangoFanFanw"
|
|
21
|
+
email = "mangofanfanw@icloud.com"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
mmccli = "mmccli:app"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.13.4"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"mypy>=2.4.0",
|
|
33
|
+
"ruff>=0.17.1",
|
|
34
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mmccli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Minecraft Java Edition Manager in Commandline."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "MangoFanFanw", email = "mangofanfanw@icloud.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"certifi>=2026.10.19",
|
|
12
|
+
"click>=8.5.0",
|
|
13
|
+
"dotenv>=0.9.9",
|
|
14
|
+
"msal>=1.39.0",
|
|
15
|
+
"pydantic>=2.14.1",
|
|
16
|
+
"pypdl>=1.5.7",
|
|
17
|
+
"requests>=2.34.2",
|
|
18
|
+
"rich>=15.0.0",
|
|
19
|
+
"typer>=0.27.3",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
mmccli = "mmccli:app"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.13.4"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"mypy>=2.4.0",
|
|
32
|
+
"ruff>=0.17.1",
|
|
33
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .app import app
|
|
@@ -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]
|