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.
@@ -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,3 @@
1
+ # mc-python-example
2
+
3
+ 使用 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,4 @@
1
+ from .game_instance import launch_game_instance
2
+ from .tools import get_mc_offline_uuid
3
+
4
+ __all__ = ["get_mc_offline_uuid", "launch_game_instance"]
@@ -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))