baka-mc-example 0.0.1__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.
- baka_mc_example-0.0.1/PKG-INFO +15 -0
- baka_mc_example-0.0.1/README.md +3 -0
- baka_mc_example-0.0.1/pyproject.toml +29 -0
- baka_mc_example-0.0.1/src/baka_mc_example/__init__.py +4 -0
- baka_mc_example-0.0.1/src/baka_mc_example/game_instance.py +220 -0
- baka_mc_example-0.0.1/src/baka_mc_example/py.typed +0 -0
- baka_mc_example-0.0.1/src/baka_mc_example/tools.py +44 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: baka-mc-example
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: BakaMCExample-Python
|
|
5
|
+
Author: HHCL233
|
|
6
|
+
Author-email: HHCL233 <XINGTAISHIJIAOXIQU@outlook.com>
|
|
7
|
+
Requires-Dist: aiofiles>=25.1.0
|
|
8
|
+
Requires-Dist: dotenv>=0.9.9
|
|
9
|
+
Requires-Dist: pydantic>=2.13.4
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# mc-python-example
|
|
14
|
+
|
|
15
|
+
使用 Python 启动、~~管理~~你的 Minecaft 实例。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "baka-mc-example"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "BakaMCExample-Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "HHCL233", email = "XINGTAISHIJIAOXIQU@outlook.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"aiofiles>=25.1.0",
|
|
12
|
+
"dotenv>=0.9.9",
|
|
13
|
+
"pydantic>=2.13.4",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.11.3,<0.12.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"mypy>=2.3.1",
|
|
23
|
+
"pytest>=9.1.1",
|
|
24
|
+
"pytest-asyncio>=1.4.0",
|
|
25
|
+
"ruff>=0.16.4",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
from asyncio.subprocess import Process
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from string import Template
|
|
7
|
+
|
|
8
|
+
import aiofiles
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from .tools import get_mc_offline_uuid, run_cmd_realtime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LaunchGameInstanceReturn(BaseModel):
|
|
15
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
proc: Process | None = None
|
|
19
|
+
error: Exception | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def launch_game_instance(
|
|
23
|
+
game_name: str,
|
|
24
|
+
user_name: str,
|
|
25
|
+
game_path: str,
|
|
26
|
+
launcher_name: str,
|
|
27
|
+
launcher_ver: str,
|
|
28
|
+
core_path: str | None = None,
|
|
29
|
+
run_path: str | None = None,
|
|
30
|
+
java_path: str | None = None,
|
|
31
|
+
natives_path: str | None = None,
|
|
32
|
+
libraries_path: str | None = None,
|
|
33
|
+
game_json_path: str | None = None,
|
|
34
|
+
assets_path: str | None = None,
|
|
35
|
+
user_uuid: str | None = None,
|
|
36
|
+
max_memory: int = 6144,
|
|
37
|
+
min_memory: int = 2048,
|
|
38
|
+
window_width: int = 854,
|
|
39
|
+
window_height: int = 480,
|
|
40
|
+
user_access_token: str = "0",
|
|
41
|
+
user_type: str = "legacy",
|
|
42
|
+
xuid: str = "",
|
|
43
|
+
client_id: str = "",
|
|
44
|
+
):
|
|
45
|
+
"""启动Minecaft
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
game_name: 实例名称。
|
|
49
|
+
user_name: 用户名称。
|
|
50
|
+
game_path: 实例路径。
|
|
51
|
+
launcher_name: 启动器名称。
|
|
52
|
+
launcher_ver: 启动器版本。
|
|
53
|
+
core_path: 游戏jar路径,默认为f"{game_path}/{game_name}.jar"。
|
|
54
|
+
java_path: Java路径,默认为"java"。
|
|
55
|
+
run_path: 游戏运行目录,默认从Path.cwd()获取。
|
|
56
|
+
natives_path: 原生依赖目录,默认为f"{game_path}/natives-{system_name}-{system_machine}"。
|
|
57
|
+
libraries_path: 依赖库目录,默认为f"{game_path}/libraries"。
|
|
58
|
+
game_json_path: 实例JSON目录,默认为f"{game_path}/{game_name}.json"
|
|
59
|
+
assets_path: 资源目录,默认为f"{game_path}/assets"。
|
|
60
|
+
user_uuid: 用户UUID,默认从用户名生成。
|
|
61
|
+
max_memory: 最大可用内存(MB),默认为6144。
|
|
62
|
+
min_memory: 最小可用内存(MB),默认为2048。
|
|
63
|
+
window_width: 游戏窗口宽度,默认为854。
|
|
64
|
+
window_height: 游戏窗口高度,默认为480。
|
|
65
|
+
user_access_token: 微软账号登录令牌,默认为"0"。
|
|
66
|
+
user_type: 用户类型,离线模式为"legacy",微软账号模式为"msa",默认为"legacy"。
|
|
67
|
+
xuid: 微软账号登录XUID,默认为""。
|
|
68
|
+
client_id: 客户端ID,默认为""。
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
# 获取系统基本信息
|
|
72
|
+
system_name = sys.platform
|
|
73
|
+
system_machine = platform.machine()
|
|
74
|
+
if system_name == "darwin":
|
|
75
|
+
system_name = "osx"
|
|
76
|
+
elif system_name == "win32":
|
|
77
|
+
system_name = "windows"
|
|
78
|
+
|
|
79
|
+
# 设置内存默认值
|
|
80
|
+
max_memory = max_memory or 6144
|
|
81
|
+
min_memory = min_memory or 2048
|
|
82
|
+
|
|
83
|
+
# 格式化路径
|
|
84
|
+
java_path = Path(java_path or "java").as_posix()
|
|
85
|
+
game_path = Path(game_path).as_posix()
|
|
86
|
+
core_path = Path(core_path or f"{game_path}/{game_name}.jar").as_posix()
|
|
87
|
+
run_path = Path(run_path or Path.cwd().as_posix()).as_posix()
|
|
88
|
+
natives_path = Path(
|
|
89
|
+
natives_path or f"{game_path}/natives-{system_name}-{system_machine}"
|
|
90
|
+
).as_posix()
|
|
91
|
+
libraries_path = Path(libraries_path or f"{game_path}/libraries").as_posix()
|
|
92
|
+
game_json_path = Path(
|
|
93
|
+
game_json_path or f"{game_path}/{game_name}.json"
|
|
94
|
+
).as_posix()
|
|
95
|
+
assets_path = Path(assets_path or f"{game_path}/assets").as_posix()
|
|
96
|
+
asset_index = "0"
|
|
97
|
+
|
|
98
|
+
# 构建UUID
|
|
99
|
+
user_uuid = user_uuid or str(get_mc_offline_uuid(user_name))
|
|
100
|
+
|
|
101
|
+
# 构建依赖库参数
|
|
102
|
+
start_libraries = []
|
|
103
|
+
libraries_split = ";" if system_name == "win32" else ":"
|
|
104
|
+
async with aiofiles.open(game_json_path, mode="r", encoding="utf-8") as f:
|
|
105
|
+
content = await f.read()
|
|
106
|
+
game_json_content = json.loads(content)
|
|
107
|
+
|
|
108
|
+
start_arguments = [
|
|
109
|
+
'"-Dfile.encoding=UTF-8"',
|
|
110
|
+
'"-Dstdout.encoding=UTF-8"',
|
|
111
|
+
'"-Dstderr.encoding=UTF-8"',
|
|
112
|
+
'"-Djava.rmi.server.useCodebaseOnly=true"',
|
|
113
|
+
'"-Dcom.sun.jndi.rmi.object.trustURLCodebase=false"',
|
|
114
|
+
'"-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=false"',
|
|
115
|
+
f' "-Dminecraft.client.jar={core_path}" ',
|
|
116
|
+
f'"-Duser.home={run_path}"',
|
|
117
|
+
'"-Djava.net.useSystemProxies=true"',
|
|
118
|
+
"-XX:+UnlockExperimentalVMOptions",
|
|
119
|
+
"-XX:+UseG1GC",
|
|
120
|
+
"-XX:G1MixedGCCountTarget=5",
|
|
121
|
+
"-XX:G1NewSizePercent=20",
|
|
122
|
+
"-XX:G1ReservePercent=20",
|
|
123
|
+
"-XX:MaxGCPauseMillis=50",
|
|
124
|
+
"-XX:G1HeapRegionSize=32m",
|
|
125
|
+
"-XX:-OmitStackTraceInFastThrow",
|
|
126
|
+
"-XX:-DontCompileHugeMethods",
|
|
127
|
+
'"-XX:MaxNodeLimit=240000"',
|
|
128
|
+
'"-XX:NodeLimitFudgeFactor=8000"',
|
|
129
|
+
"-XX:TieredCompileTaskTimeout=10000",
|
|
130
|
+
'"-XX:ReservedCodeCacheSize=400M"',
|
|
131
|
+
'"-XX:NonNMethodCodeHeapSize=12M"',
|
|
132
|
+
'"-XX:ProfiledCodeHeapSize=194M"',
|
|
133
|
+
'"-XX:NmethodSweepActivity=1"',
|
|
134
|
+
"--sun-misc-unsafe-memory-access=allow",
|
|
135
|
+
'"-Dfml.ignoreInvalidMinecraftCertificates=true"',
|
|
136
|
+
'"-Dfml.ignorePatchDiscrepancies=true"',
|
|
137
|
+
]
|
|
138
|
+
main_class = game_json_content.get("mainClass", "")
|
|
139
|
+
temp_game_arguments = game_json_content.get("arguments", {}).get("game", [])
|
|
140
|
+
temp_jvm_arguments = game_json_content.get("arguments", {}).get("jvm", [])
|
|
141
|
+
asset_index = game_json_content.get("assetIndex", {}).get("id", "0")
|
|
142
|
+
libraries_infos = game_json_content.get("libraries", [])
|
|
143
|
+
game_ver = game_json_content.get("id", "")
|
|
144
|
+
game_jar = game_json_content.get("jar", "")
|
|
145
|
+
|
|
146
|
+
def replace_arguments_placeholder(replace_str: str):
|
|
147
|
+
result = Template(replace_str).safe_substitute(
|
|
148
|
+
natives_directory=natives_path,
|
|
149
|
+
launcher_name=launcher_name,
|
|
150
|
+
launcher_version=launcher_ver,
|
|
151
|
+
classpath=libraries_split.join(unique_libraries),
|
|
152
|
+
resolution_width=window_width,
|
|
153
|
+
resolution_height=window_height,
|
|
154
|
+
auth_player_name=user_name,
|
|
155
|
+
version_name=game_ver,
|
|
156
|
+
game_directory=game_path,
|
|
157
|
+
assets_root=assets_path,
|
|
158
|
+
assets_index_name=asset_index,
|
|
159
|
+
auth_uuid=user_uuid,
|
|
160
|
+
auth_access_token=user_access_token,
|
|
161
|
+
clientid=client_id,
|
|
162
|
+
auth_xuid=xuid,
|
|
163
|
+
user_type=user_type,
|
|
164
|
+
version_type=f"{launcher_name} {launcher_ver}",
|
|
165
|
+
library_directory=libraries_path,
|
|
166
|
+
primary_jar_name=f"{game_jar}.jar",
|
|
167
|
+
classpath_separator=libraries_split,
|
|
168
|
+
fullscreen=False,
|
|
169
|
+
)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
# 解析依赖库
|
|
173
|
+
for librarie in libraries_infos:
|
|
174
|
+
for rule in librarie.get("rules", []):
|
|
175
|
+
os_name = rule.get("os", {}).get("name", "")
|
|
176
|
+
if (rule.get("action") == "allow") and (system_name != os_name):
|
|
177
|
+
break
|
|
178
|
+
if librarie.get("downloads"):
|
|
179
|
+
librarie_path = f"{libraries_path}/{librarie.get('downloads', {}).get('artifact', {}).get('path', '')}"
|
|
180
|
+
else:
|
|
181
|
+
librarie_name: str = librarie.get("name", "")
|
|
182
|
+
librarie_name_list = librarie_name.split(":")
|
|
183
|
+
librarie_relative_path = f"{librarie_name_list[0].replace('.', '/')}/{librarie_name_list[1]}/{librarie_name_list[2]}/{librarie_name_list[1]}-{librarie_name_list[2]}.jar"
|
|
184
|
+
librarie_path = f"{libraries_path}/{librarie_relative_path}"
|
|
185
|
+
start_libraries.append(librarie_path)
|
|
186
|
+
start_libraries.append(core_path)
|
|
187
|
+
unique_libraries = list(dict.fromkeys(start_libraries))
|
|
188
|
+
|
|
189
|
+
# 添加从json内获取的参数
|
|
190
|
+
for arguments in (temp_jvm_arguments, temp_game_arguments):
|
|
191
|
+
for argument in arguments:
|
|
192
|
+
if isinstance(argument, str):
|
|
193
|
+
start_arguments.append(replace_arguments_placeholder(argument))
|
|
194
|
+
else:
|
|
195
|
+
rules = argument.get("rules", [])
|
|
196
|
+
rules_allow = False
|
|
197
|
+
for rule in rules:
|
|
198
|
+
if (rule.get("action", "") == "allow") and (
|
|
199
|
+
rule.get("features")
|
|
200
|
+
or rule.get("os", {}).get("name", "") != system_name
|
|
201
|
+
):
|
|
202
|
+
rules_allow = False
|
|
203
|
+
break
|
|
204
|
+
else:
|
|
205
|
+
rules_allow = True
|
|
206
|
+
if rules_allow:
|
|
207
|
+
for value in argument.get("value", []):
|
|
208
|
+
start_arguments.append(replace_arguments_placeholder(value))
|
|
209
|
+
if main_class not in start_arguments:
|
|
210
|
+
start_arguments.append(main_class)
|
|
211
|
+
|
|
212
|
+
# 构建完整命令
|
|
213
|
+
start_cmd = f"{java_path} -Xms{min_memory}m -Xmx{max_memory}m {' '.join(start_arguments)}"
|
|
214
|
+
print(start_cmd)
|
|
215
|
+
# 启动游戏
|
|
216
|
+
game_proc = await run_cmd_realtime(start_cmd)
|
|
217
|
+
return LaunchGameInstanceReturn(success=True, proc=game_proc)
|
|
218
|
+
except Exception as e: # noqa: BLE001
|
|
219
|
+
# 错误处理
|
|
220
|
+
return LaunchGameInstanceReturn(success=False, error=e)
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def run_cmd_realtime(cmd: str):
|
|
7
|
+
# 创建异步子进程
|
|
8
|
+
proc = await asyncio.create_subprocess_shell(
|
|
9
|
+
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
return proc
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
# 实时读取标准输出
|
|
16
|
+
async def read_stream(stream, prefix="[stdout]"):
|
|
17
|
+
async for line in stream:
|
|
18
|
+
# line 是 bytes 类型,需要 decode
|
|
19
|
+
print(f"{prefix} {line.decode().rstrip()}")
|
|
20
|
+
|
|
21
|
+
# 同时实时读取 stdout 和 stderr
|
|
22
|
+
await asyncio.gather(
|
|
23
|
+
read_stream(proc.stdout, "[stdout]"), read_stream(proc.stderr, "[stderr]")
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# 等待子进程退出并获取返回码
|
|
28
|
+
await proc.wait()
|
|
29
|
+
print(f"[{cmd!r} exited with {proc.returncode}]")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_mc_offline_uuid(username: str) -> uuid.UUID:
|
|
34
|
+
# 构造字符串前缀
|
|
35
|
+
target_str = f"OfflinePlayer:{username}"
|
|
36
|
+
|
|
37
|
+
# 计算MD5哈希的16字节原始数据
|
|
38
|
+
hash_bytes = bytearray(hashlib.md5(target_str.encode("utf-8")).digest())
|
|
39
|
+
|
|
40
|
+
# 设置UUID标志位
|
|
41
|
+
hash_bytes[6] = (hash_bytes[6] & 0x0F) | 0x30 # Version 3
|
|
42
|
+
hash_bytes[8] = (hash_bytes[8] & 0x3F) | 0x80 # Variant RFC 4122
|
|
43
|
+
|
|
44
|
+
return uuid.UUID(bytes=bytes(hash_bytes))
|