orzmc 2.0.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.
- orzmc/__init__.py +171 -0
- orzmc/core/client/__init__.py +21 -0
- orzmc/core/client/base.py +85 -0
- orzmc/core/client/fabric.py +28 -0
- orzmc/core/client/forge.py +128 -0
- orzmc/core/client/vanilla.py +17 -0
- orzmc/core/fabric.py +97 -0
- orzmc/core/forge.py +44 -0
- orzmc/core/mojang.py +123 -0
- orzmc/core/profiles.py +24 -0
- orzmc/core/server/__init__.py +23 -0
- orzmc/core/server/base.py +80 -0
- orzmc/core/server/fabric.py +79 -0
- orzmc/core/server/forge.py +85 -0
- orzmc/core/server/paper.py +75 -0
- orzmc/core/server/vanilla.py +26 -0
- orzmc/domain/java.py +24 -0
- orzmc/domain/launch.py +209 -0
- orzmc/domain/libraries.py +157 -0
- orzmc/domain/options.py +35 -0
- orzmc/domain/paths.py +146 -0
- orzmc/domain/types.py +44 -0
- orzmc/infra/fs.py +75 -0
- orzmc/infra/hashing.py +17 -0
- orzmc/infra/http.py +78 -0
- orzmc/infra/log.py +91 -0
- orzmc/infra/progress.py +79 -0
- orzmc/infra/runner.py +142 -0
- orzmc/py.typed +0 -0
- orzmc/services/backup.py +39 -0
- orzmc/services/client.py +167 -0
- orzmc/services/context.py +82 -0
- orzmc/services/downloader.py +171 -0
- orzmc/services/java.py +131 -0
- orzmc/services/server.py +139 -0
- orzmc/services/versions.py +80 -0
- orzmc/version.py +3 -0
- orzmc-2.0.0.dist-info/METADATA +32 -0
- orzmc-2.0.0.dist-info/RECORD +40 -0
- orzmc-2.0.0.dist-info/WHEEL +4 -0
orzmc/__init__.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""OrzMC core library.
|
|
2
|
+
|
|
3
|
+
This module is the **public API contract**. The application layer (orzmc-app)
|
|
4
|
+
and any external project should only import from here; internals live under
|
|
5
|
+
``domain`` / ``infra`` / ``core`` / ``services``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from orzmc.core.mojang import VersionEntry
|
|
13
|
+
from orzmc.domain.java import DEFAULT_JAVA_MAJOR, required_java_major
|
|
14
|
+
from orzmc.domain.launch import DEFAULT_MAIN_CLASS, build_launch_command, game_args, jvm_args
|
|
15
|
+
from orzmc.domain.libraries import Library, resolve_libraries
|
|
16
|
+
from orzmc.domain.options import RuntimeOptions
|
|
17
|
+
from orzmc.domain.paths import DEFAULT_ROOT, PathLayout
|
|
18
|
+
from orzmc.domain.types import GameType
|
|
19
|
+
from orzmc.infra.fs import FileStore
|
|
20
|
+
from orzmc.infra.log import NullReporter, Reporter, RichReporter
|
|
21
|
+
from orzmc.infra.progress import NullProgress, ProgressSink, RichProgress
|
|
22
|
+
from orzmc.services.backup import Backup
|
|
23
|
+
from orzmc.services.client import ClientService
|
|
24
|
+
from orzmc.services.context import AppContext, Services
|
|
25
|
+
from orzmc.services.server import ServerService
|
|
26
|
+
from orzmc.services.versions import InstalledVersion, VersionManager
|
|
27
|
+
from orzmc.version import __version__
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DEFAULT_JAVA_MAJOR",
|
|
31
|
+
"DEFAULT_MAIN_CLASS",
|
|
32
|
+
"DEFAULT_ROOT",
|
|
33
|
+
"AppContext",
|
|
34
|
+
"Backup",
|
|
35
|
+
"ClientService",
|
|
36
|
+
"FileStore",
|
|
37
|
+
"GameType",
|
|
38
|
+
"InstalledVersion",
|
|
39
|
+
"Library",
|
|
40
|
+
"NullProgress",
|
|
41
|
+
"NullReporter",
|
|
42
|
+
"PathLayout",
|
|
43
|
+
"ProgressSink",
|
|
44
|
+
# protocols & impls
|
|
45
|
+
"Reporter",
|
|
46
|
+
"RichProgress",
|
|
47
|
+
"RichReporter",
|
|
48
|
+
# domain
|
|
49
|
+
"RuntimeOptions",
|
|
50
|
+
"ServerService",
|
|
51
|
+
# services
|
|
52
|
+
"Services",
|
|
53
|
+
"VersionEntry",
|
|
54
|
+
"VersionManager",
|
|
55
|
+
# versions
|
|
56
|
+
"__version__",
|
|
57
|
+
"backup_world",
|
|
58
|
+
"build_launch_command",
|
|
59
|
+
"deploy_server",
|
|
60
|
+
"game_args",
|
|
61
|
+
"install_java",
|
|
62
|
+
"jvm_args",
|
|
63
|
+
# entry points
|
|
64
|
+
"launch_client",
|
|
65
|
+
"list_versions",
|
|
66
|
+
"remote_version_catalog",
|
|
67
|
+
"remote_versions",
|
|
68
|
+
"remove_version",
|
|
69
|
+
"required_java_major",
|
|
70
|
+
"resolve_libraries",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# ── entry points ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def launch_client(
|
|
77
|
+
options: RuntimeOptions,
|
|
78
|
+
reporter: Reporter | None = None,
|
|
79
|
+
sink: ProgressSink | None = None,
|
|
80
|
+
confirm_java: Callable[[int, bool], bool] | None = None,
|
|
81
|
+
) -> int:
|
|
82
|
+
"""Launch the Minecraft client for ``options`` (auto-installs everything).
|
|
83
|
+
|
|
84
|
+
``confirm_java`` (optional) is consulted before installing a Java runtime.
|
|
85
|
+
"""
|
|
86
|
+
base = Services(options, reporter=reporter, sink=sink)
|
|
87
|
+
version = options.version or base.mojang.latest_release_id()
|
|
88
|
+
if not version:
|
|
89
|
+
raise RuntimeError("无法确定 Minecraft 版本")
|
|
90
|
+
return ClientService(base.for_version(version)).run(confirm_java=confirm_java)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def deploy_server(
|
|
94
|
+
options: RuntimeOptions,
|
|
95
|
+
reporter: Reporter | None = None,
|
|
96
|
+
sink: ProgressSink | None = None,
|
|
97
|
+
confirm_java: Callable[[int, bool], bool] | None = None,
|
|
98
|
+
confirm_eula: Callable[[], bool] | None = None,
|
|
99
|
+
on_line: Callable[[str], None] | None = None,
|
|
100
|
+
) -> int:
|
|
101
|
+
"""Deploy and run a server for ``options`` (auto-installs everything).
|
|
102
|
+
|
|
103
|
+
``confirm_eula`` (optional) accepts the Minecraft EULA interactively when
|
|
104
|
+
``options.yes`` is not set. ``on_line`` receives streamed server output.
|
|
105
|
+
"""
|
|
106
|
+
base = Services(options, reporter=reporter, sink=sink)
|
|
107
|
+
version = options.version or base.mojang.latest_release_id()
|
|
108
|
+
if not version:
|
|
109
|
+
raise RuntimeError("无法确定 Minecraft 版本")
|
|
110
|
+
return ServerService(base.for_version(version)).run(
|
|
111
|
+
confirm_java=confirm_java, confirm_eula=confirm_eula, on_line=on_line
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def list_versions(root_dir: str | None = None) -> list[InstalledVersion]:
|
|
116
|
+
"""Enumerate installed versions (client / server types) under ``root_dir``."""
|
|
117
|
+
return VersionManager(root_dir=root_dir).list_versions()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def remote_version_catalog(root_dir: str | None = None, update: bool = False) -> list[VersionEntry]:
|
|
121
|
+
"""List all Mojang manifest versions (any type, newest first); installs nothing."""
|
|
122
|
+
options = RuntimeOptions(root_dir=root_dir or DEFAULT_ROOT)
|
|
123
|
+
return Services(options).mojang.version_entries(update=update)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def remote_versions(root_dir: str | None = None, update: bool = False) -> list[str]:
|
|
127
|
+
"""List Mojang release version ids (fetches/caches the manifest, installs nothing)."""
|
|
128
|
+
return [e.id for e in remote_version_catalog(root_dir, update) if e.is_release]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def remove_version(
|
|
132
|
+
version: str,
|
|
133
|
+
*,
|
|
134
|
+
is_client: bool = True,
|
|
135
|
+
game_type: str | None = None,
|
|
136
|
+
root_dir: str | None = None,
|
|
137
|
+
yes: bool = False,
|
|
138
|
+
reporter: Reporter | None = None,
|
|
139
|
+
confirm: Callable[[str], bool] | None = None,
|
|
140
|
+
) -> bool:
|
|
141
|
+
"""Remove a version's client or a specific server type. ``confirm`` is
|
|
142
|
+
consulted unless ``yes`` is set."""
|
|
143
|
+
manager = VersionManager(root_dir=root_dir)
|
|
144
|
+
return manager.remove(version, is_client=is_client, game_type=game_type, yes=yes, confirm=confirm)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def backup_world(
|
|
148
|
+
version: str,
|
|
149
|
+
game_type: str = "vanilla",
|
|
150
|
+
root_dir: str | None = None,
|
|
151
|
+
reporter: Reporter | None = None,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Back up ``versions/<version>/server/<game_type>/world`` to ``backup/worlds/``."""
|
|
154
|
+
return Backup(reporter=reporter, root_dir=root_dir).backup_world(version, game_type)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def install_java(
|
|
158
|
+
major: int,
|
|
159
|
+
*,
|
|
160
|
+
need_jdk: bool = False,
|
|
161
|
+
root_dir: str | None = None,
|
|
162
|
+
reporter: Reporter | None = None,
|
|
163
|
+
sink: ProgressSink | None = None,
|
|
164
|
+
confirm: Callable[[int, bool], bool] | None = None,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Ensure a sandboxed Java ``major`` is installed under ``root_dir/java/``.
|
|
167
|
+
|
|
168
|
+
Returns the managed ``bin/java`` path.
|
|
169
|
+
"""
|
|
170
|
+
options = RuntimeOptions(root_dir=root_dir or DEFAULT_ROOT)
|
|
171
|
+
return Services(options, reporter=reporter, sink=sink).java_env.resolve(major, need_jdk=need_jdk, confirm=confirm)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Client add-on strategies (``ClientProvider`` registry).
|
|
2
|
+
|
|
3
|
+
Importing this package registers the vanilla / fabric / forge providers so
|
|
4
|
+
``ClientProvider.for_type`` resolves them. Library-internal — none of these
|
|
5
|
+
names are part of the public API contract in ``orzmc/__init__``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from orzmc.core.client.base import BuildJavaSeam, ClientPrepare, ClientProvider, DownloadSeam
|
|
9
|
+
from orzmc.core.client.fabric import FabricProvider
|
|
10
|
+
from orzmc.core.client.forge import ForgeProvider
|
|
11
|
+
from orzmc.core.client.vanilla import VanillaProvider
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BuildJavaSeam",
|
|
15
|
+
"ClientPrepare",
|
|
16
|
+
"ClientProvider",
|
|
17
|
+
"DownloadSeam",
|
|
18
|
+
"FabricProvider",
|
|
19
|
+
"ForgeProvider",
|
|
20
|
+
"VanillaProvider",
|
|
21
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""ClientProvider strategy: resolve an optional launch addon for a game type.
|
|
2
|
+
|
|
3
|
+
Mirror of ``core/server``: providers receive every dependency as an injected
|
|
4
|
+
seam (``ClientPrepare``), so this package never imports ``orzmc.services``.
|
|
5
|
+
The orchestration seams ``download`` / ``resolve_build_java`` are supplied by
|
|
6
|
+
``ClientService`` from the services layer — a textbook dependency inversion.
|
|
7
|
+
|
|
8
|
+
Each concrete provider holds all knowledge of one client type in a single
|
|
9
|
+
file, so editing one type never affects another: vanilla is a no-op, fabric
|
|
10
|
+
wraps the fabric-meta profile json, forge runs the official installer and
|
|
11
|
+
harvests its generated client jar.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Protocol
|
|
20
|
+
|
|
21
|
+
from orzmc.core.profiles import ProfileAddon
|
|
22
|
+
from orzmc.domain.paths import PathLayout
|
|
23
|
+
from orzmc.domain.types import GameType
|
|
24
|
+
from orzmc.infra.fs import FileStore
|
|
25
|
+
from orzmc.infra.http import HttpClient
|
|
26
|
+
from orzmc.infra.log import Reporter
|
|
27
|
+
from orzmc.infra.runner import ProcessRunner
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DownloadSeam(Protocol):
|
|
31
|
+
"""Single-file download that skips an already-valid cache (download_file)."""
|
|
32
|
+
|
|
33
|
+
def __call__(self, url: str, dest: str, desc: str, sha1: str | None = None, force: bool = False) -> bool: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BuildJavaSeam(Protocol):
|
|
37
|
+
"""Resolve a sandboxed java binary for an install step (JavaEnv.resolve)."""
|
|
38
|
+
|
|
39
|
+
def __call__(
|
|
40
|
+
self, major: int, need_jdk: bool = False, confirm: Callable[[int, bool], bool] | None = None
|
|
41
|
+
) -> str: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ClientPrepare:
|
|
46
|
+
"""Injected seams + parsed Mojang data a ClientProvider needs for an addon."""
|
|
47
|
+
|
|
48
|
+
version: str
|
|
49
|
+
version_json: dict[str, Any]
|
|
50
|
+
major: int
|
|
51
|
+
force_download: bool
|
|
52
|
+
confirm_java: Callable[[int, bool], bool] | None
|
|
53
|
+
# ── infra / domain seams ────────────────────────────────────────────────
|
|
54
|
+
paths: PathLayout
|
|
55
|
+
fs: FileStore
|
|
56
|
+
reporter: Reporter
|
|
57
|
+
http: HttpClient
|
|
58
|
+
process: ProcessRunner
|
|
59
|
+
# ── orchestration injections (services layer) ───────────────────────────
|
|
60
|
+
download: DownloadSeam
|
|
61
|
+
resolve_build_java: BuildJavaSeam
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_REGISTRY: dict[GameType, ClientProvider] = {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ClientProvider(ABC):
|
|
68
|
+
"""Strategy that produces a launch addon for ``game_type`` (or None).
|
|
69
|
+
|
|
70
|
+
Concrete providers are stateless singletons that self-register on import.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
game_type: GameType
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def addon(self, prepare: ClientPrepare) -> ProfileAddon | None:
|
|
77
|
+
"""Resolve extra libraries/args/main-class; return None to launch vanilla."""
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def register(cls, provider: ClientProvider) -> None:
|
|
81
|
+
_REGISTRY[provider.game_type] = provider
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def for_type(cls, game_type: GameType) -> ClientProvider | None:
|
|
85
|
+
return _REGISTRY.get(game_type)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Fabric client: resolve the fabric-loader profile and download its libraries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from orzmc.core.client.base import ClientPrepare, ClientProvider
|
|
6
|
+
from orzmc.core.fabric import Fabric
|
|
7
|
+
from orzmc.core.profiles import ProfileAddon
|
|
8
|
+
from orzmc.domain.types import GameType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FabricProvider(ClientProvider):
|
|
12
|
+
game_type = GameType.FABRIC
|
|
13
|
+
|
|
14
|
+
def addon(self, prepare: ClientPrepare) -> ProfileAddon | None:
|
|
15
|
+
addon = Fabric(prepare.http, prepare.version).profile()
|
|
16
|
+
for lib in addon.libraries:
|
|
17
|
+
if lib.url:
|
|
18
|
+
prepare.download(
|
|
19
|
+
lib.url,
|
|
20
|
+
prepare.paths.client_library_path(lib.path),
|
|
21
|
+
f"下载 {lib.name}",
|
|
22
|
+
lib.sha1,
|
|
23
|
+
force=prepare.force_download,
|
|
24
|
+
)
|
|
25
|
+
return addon
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ClientProvider.register(FabricProvider())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Forge client: run the official installer and assemble the launch addon.
|
|
2
|
+
|
|
3
|
+
Forge ships no standalone client definition: the launch json is embedded in
|
|
4
|
+
the installer jar and the patched ``client`` classifier jar (the Minecraft
|
|
5
|
+
module the launcher needs) has **no downloadable source** — its ``url`` is
|
|
6
|
+
empty in ``version.json`` and Maven 404s it. It is produced by the installer's
|
|
7
|
+
binary-patching pipeline from the vanilla client jar.
|
|
8
|
+
|
|
9
|
+
So this provider mirrors the server side: download the installer, run it
|
|
10
|
+
headlessly (``--installClient``) into a temporary official-layout directory,
|
|
11
|
+
then harvest the generated jar plus the forge libraries into our own
|
|
12
|
+
``client/libraries/`` layout. The shared ``core.forge`` adapter resolves the
|
|
13
|
+
version from ``promotions_slim.json`` and reads ``version.json`` from the jar.
|
|
14
|
+
|
|
15
|
+
Only the modern ForgeBootstrap era (1.20.4+ / 49.x) is supported: those
|
|
16
|
+
versions ship a plain ``arguments`` block with no ``{{token}}`` templating and
|
|
17
|
+
exactly one generated jar. Older Forge (bootstraplauncher) versions raise a
|
|
18
|
+
clear error instead of producing a broken launch.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from orzmc.core.client.base import ClientPrepare, ClientProvider
|
|
27
|
+
from orzmc.core.forge import Forge, extract_version_json
|
|
28
|
+
from orzmc.core.profiles import ProfileAddon
|
|
29
|
+
from orzmc.domain.libraries import Library
|
|
30
|
+
from orzmc.domain.types import GameType
|
|
31
|
+
|
|
32
|
+
_BOOTSTRAP_LAUNCHER = "cpw.mods.bootstraplauncher"
|
|
33
|
+
_FORGE_MIN_JAVA = 17 # ForgeBootstrap-era installers require Java 17+
|
|
34
|
+
|
|
35
|
+
_MINIMAL_LAUNCHER_PROFILE = {
|
|
36
|
+
"authenticationDatabase": {},
|
|
37
|
+
"selectedUser": {"account": "", "profile": ""},
|
|
38
|
+
"profiles": {},
|
|
39
|
+
"settings": {"enableSnapshots": False, "keepLauncherOpen": False},
|
|
40
|
+
"version": 1,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ForgeProvider(ClientProvider):
|
|
45
|
+
game_type = GameType.FORGE
|
|
46
|
+
|
|
47
|
+
def addon(self, prepare: ClientPrepare) -> ProfileAddon | None:
|
|
48
|
+
forge = Forge(prepare.http)
|
|
49
|
+
full = forge.latest_full_version(prepare.version)
|
|
50
|
+
installer = os.path.join(prepare.paths.client_dir(), "forge", f"forge-{full}-installer.jar")
|
|
51
|
+
prepare.download(forge.installer_url(full), installer, f"下载 Forge 安装器 {full}")
|
|
52
|
+
|
|
53
|
+
forge_json = extract_version_json(installer)
|
|
54
|
+
if _BOOTSTRAP_LAUNCHER in str(forge_json.get("mainClass", "")):
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"Forge {full} 使用旧版启动器(BootstrapLauncher),暂不支持客户端启动"
|
|
57
|
+
"(仅支持 ForgeBootstrap 系,1.20.4 及以上)"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
install_dir = os.path.join(prepare.paths.client_dir(), "forge", "install")
|
|
61
|
+
try:
|
|
62
|
+
self._run_client_install(prepare, installer, full, install_dir)
|
|
63
|
+
libraries = self._harvest_libraries(prepare, forge_json, install_dir)
|
|
64
|
+
finally:
|
|
65
|
+
prepare.fs.remove(install_dir)
|
|
66
|
+
|
|
67
|
+
return ProfileAddon(
|
|
68
|
+
libraries=libraries,
|
|
69
|
+
jvm_args=_forge_string_args(forge_json, "jvm"),
|
|
70
|
+
game_args=_forge_string_args(forge_json, "game"),
|
|
71
|
+
main_class=forge_json.get("mainClass"),
|
|
72
|
+
# the harvested client jar *is* the game jar; the vanilla jar must
|
|
73
|
+
# not also sit on the classpath or its unpatched classes shadow it
|
|
74
|
+
uses_own_client_jar=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# ── internals ───────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def _run_client_install(self, prepare: ClientPrepare, installer: str, full: str, install_dir: str) -> None:
|
|
80
|
+
prepare.fs.ensure_dir(install_dir)
|
|
81
|
+
profile = os.path.join(install_dir, "launcher_profiles.json")
|
|
82
|
+
if not prepare.fs.is_file(profile):
|
|
83
|
+
prepare.fs.write_json(profile, _MINIMAL_LAUNCHER_PROFILE)
|
|
84
|
+
java_bin = prepare.resolve_build_java(
|
|
85
|
+
max(prepare.major, _FORGE_MIN_JAVA), need_jdk=False, confirm=prepare.confirm_java
|
|
86
|
+
)
|
|
87
|
+
prepare.reporter.info(f"正在生成 Forge 客户端 {full}...")
|
|
88
|
+
code = prepare.process.run_stream(
|
|
89
|
+
[java_bin, "-jar", installer, "--installClient", install_dir],
|
|
90
|
+
on_line=lambda line: prepare.reporter.plain(line),
|
|
91
|
+
cwd=install_dir,
|
|
92
|
+
)
|
|
93
|
+
if code != 0:
|
|
94
|
+
raise RuntimeError(f"Forge 客户端安装失败 ({full})")
|
|
95
|
+
|
|
96
|
+
def _harvest_libraries(self, prepare: ClientPrepare, forge_json: dict[str, Any], install_dir: str) -> list[Library]:
|
|
97
|
+
"""Move the forge libraries (incl. the generated client jar) into our layout."""
|
|
98
|
+
libraries: list[Library] = []
|
|
99
|
+
for lib in forge_json.get("libraries", []):
|
|
100
|
+
artifact = (lib.get("downloads") or {}).get("artifact") or {}
|
|
101
|
+
path = artifact.get("path")
|
|
102
|
+
name = lib.get("name")
|
|
103
|
+
if not path or not name:
|
|
104
|
+
continue
|
|
105
|
+
src = os.path.join(install_dir, "libraries", path)
|
|
106
|
+
if not prepare.fs.is_file(src):
|
|
107
|
+
continue
|
|
108
|
+
prepare.fs.move(src, prepare.paths.client_library_path(path))
|
|
109
|
+
libraries.append(
|
|
110
|
+
Library(
|
|
111
|
+
name=name,
|
|
112
|
+
path=path,
|
|
113
|
+
url=artifact.get("url") or "",
|
|
114
|
+
sha1=artifact.get("sha1"),
|
|
115
|
+
size=artifact.get("size"),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
if not any(lib.path.endswith("-client.jar") for lib in libraries):
|
|
119
|
+
raise RuntimeError("未找到 Forge 客户端产物(client jar),安装可能不完整")
|
|
120
|
+
return libraries
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _forge_string_args(forge_json: dict[str, Any], key: str) -> list[str]:
|
|
124
|
+
"""Plain-string entries of ``arguments.<key>`` (forge ships no rule dicts here)."""
|
|
125
|
+
return [a for a in (forge_json.get("arguments") or {}).get(key, []) if isinstance(a, str)]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
ClientProvider.register(ForgeProvider())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Vanilla client: no add-on — the plain Mojang launch definition suffices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from orzmc.core.client.base import ClientPrepare, ClientProvider
|
|
6
|
+
from orzmc.core.profiles import ProfileAddon
|
|
7
|
+
from orzmc.domain.types import GameType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VanillaProvider(ClientProvider):
|
|
11
|
+
game_type = GameType.VANILLA
|
|
12
|
+
|
|
13
|
+
def addon(self, prepare: ClientPrepare) -> ProfileAddon | None:
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ClientProvider.register(VanillaProvider())
|
orzmc/core/fabric.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Fabric loader resolution & library download (fabric-meta API)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from orzmc.core.profiles import ProfileAddon
|
|
8
|
+
from orzmc.domain.libraries import Library
|
|
9
|
+
from orzmc.infra.http import HttpClient
|
|
10
|
+
|
|
11
|
+
META_BASE = "https://meta.fabricmc.net/v2"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Fabric:
|
|
15
|
+
def __init__(self, http: HttpClient, version: str, loader: str | None = None) -> None:
|
|
16
|
+
self._http = http
|
|
17
|
+
self.version = version
|
|
18
|
+
self.loader = loader
|
|
19
|
+
|
|
20
|
+
def profile(self) -> ProfileAddon:
|
|
21
|
+
"""Resolve the fabric-loader profile json for this MC version."""
|
|
22
|
+
loader_version = self.loader or self.latest_loader_version()
|
|
23
|
+
# fabric-meta stopped accepting the installer version in this URL (404
|
|
24
|
+
# for every combo); the loader version alone resolves the profile.
|
|
25
|
+
url = f"{META_BASE}/versions/loader/{self.version}/{loader_version}/profile/json"
|
|
26
|
+
config: dict[str, Any] = self._http.get_json(url)
|
|
27
|
+
|
|
28
|
+
libraries: list[Library] = []
|
|
29
|
+
for lib in config.get("libraries", []):
|
|
30
|
+
name = lib.get("name")
|
|
31
|
+
if not name:
|
|
32
|
+
continue
|
|
33
|
+
# fabric-meta's `url` is now just the repository base (e.g.
|
|
34
|
+
# ``https://maven.fabricmc.net/``), not a per-artifact url — the
|
|
35
|
+
# local path must come from the maven coordinates themselves.
|
|
36
|
+
path = _coordinate_path(name)
|
|
37
|
+
base = lib.get("url") or "https://maven.fabricmc.net/"
|
|
38
|
+
libraries.append(
|
|
39
|
+
Library(
|
|
40
|
+
name=name,
|
|
41
|
+
path=path,
|
|
42
|
+
url=base.rstrip("/") + "/" + path,
|
|
43
|
+
sha1=lib.get("sha1"),
|
|
44
|
+
size=lib.get("size"),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
arguments = config.get("arguments", {})
|
|
49
|
+
jvm_args = [a for a in arguments.get("jvm", []) if isinstance(a, str)]
|
|
50
|
+
game_args = [a for a in arguments.get("game", []) if isinstance(a, str)]
|
|
51
|
+
main_class = _main_class(config)
|
|
52
|
+
|
|
53
|
+
return ProfileAddon(libraries=libraries, jvm_args=jvm_args, game_args=game_args, main_class=main_class)
|
|
54
|
+
|
|
55
|
+
def latest_loader_version(self) -> str:
|
|
56
|
+
"""Latest stable fabric-loader version for this MC version."""
|
|
57
|
+
entries = self._http.get_json(f"{META_BASE}/versions/loader/{self.version}")
|
|
58
|
+
for entry in entries:
|
|
59
|
+
loader = (entry or {}).get("loader", {})
|
|
60
|
+
if loader.get("stable"):
|
|
61
|
+
return loader["version"]
|
|
62
|
+
if entries:
|
|
63
|
+
return entries[0]["loader"]["version"]
|
|
64
|
+
raise RuntimeError(f"Fabric 不支持 Minecraft {self.version}")
|
|
65
|
+
|
|
66
|
+
def latest_installer_version(self) -> str:
|
|
67
|
+
"""Latest stable fabric-installer version."""
|
|
68
|
+
entries = self._http.get_json(f"{META_BASE}/versions/installer")
|
|
69
|
+
for entry in entries:
|
|
70
|
+
if entry.get("stable"):
|
|
71
|
+
return entry["version"]
|
|
72
|
+
return entries[0]["version"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _coordinate_path(name: str) -> str:
|
|
76
|
+
"""Map maven coordinates ``group:artifact:version[:classifier]`` to a jar path."""
|
|
77
|
+
parts = name.split(":")
|
|
78
|
+
if len(parts) == 3:
|
|
79
|
+
group, artifact, version = parts
|
|
80
|
+
classifier = ""
|
|
81
|
+
elif len(parts) == 4:
|
|
82
|
+
group, artifact, version, classifier = parts
|
|
83
|
+
else:
|
|
84
|
+
return name.replace(":", "/") + ".jar"
|
|
85
|
+
base = f"{artifact}-{version}"
|
|
86
|
+
if classifier:
|
|
87
|
+
base += f"-{classifier}"
|
|
88
|
+
return f"{group.replace('.', '/')}/{artifact}/{version}/{base}.jar"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _main_class(config: dict[str, Any]) -> str | None:
|
|
92
|
+
main_class = config.get("mainClass")
|
|
93
|
+
if isinstance(main_class, str):
|
|
94
|
+
return main_class
|
|
95
|
+
if isinstance(main_class, dict):
|
|
96
|
+
return main_class.get("client") or main_class.get("server")
|
|
97
|
+
return None
|
orzmc/core/forge.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Forge version resolution & installer helpers (Maven API — no HTML scraping).
|
|
2
|
+
|
|
3
|
+
The download page's own data source, ``promotions_slim.json``, maps a Minecraft
|
|
4
|
+
version to its recommended/latest Forge build. Everything else comes from the
|
|
5
|
+
Forge Maven repository, and the client launch definition (``version.json``) is
|
|
6
|
+
extracted from the installer jar it is shipped inside. Shared by the client and
|
|
7
|
+
server Forge providers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import zipfile
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from orzmc.infra.http import HttpClient
|
|
17
|
+
|
|
18
|
+
PROMOTIONS_URL = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json"
|
|
19
|
+
MAVEN_BASE = "https://maven.minecraftforge.net/net/minecraftforge/forge"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Forge:
|
|
23
|
+
"""Resolve the Forge version for a MC version and locate installer artifacts."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, http: HttpClient) -> None:
|
|
26
|
+
self._http = http
|
|
27
|
+
|
|
28
|
+
def latest_full_version(self, mc_version: str) -> str:
|
|
29
|
+
"""``<mc>-<build>`` (e.g. ``1.20.4-49.2.8``) for ``mc_version``."""
|
|
30
|
+
promos = self._http.get_json(PROMOTIONS_URL).get("promos") or {}
|
|
31
|
+
for key in (f"{mc_version}-latest", f"{mc_version}-recommended"):
|
|
32
|
+
build = promos.get(key)
|
|
33
|
+
if build:
|
|
34
|
+
return f"{mc_version}-{build}"
|
|
35
|
+
raise RuntimeError(f"未找到 Minecraft {mc_version} 的 Forge 版本")
|
|
36
|
+
|
|
37
|
+
def installer_url(self, full_version: str) -> str:
|
|
38
|
+
return f"{MAVEN_BASE}/{full_version}/forge-{full_version}-installer.jar"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def extract_version_json(installer_path: str) -> dict[str, Any]:
|
|
42
|
+
"""Read the ``version.json`` embedded in a Forge installer jar."""
|
|
43
|
+
with zipfile.ZipFile(installer_path) as zf, zf.open("version.json") as f:
|
|
44
|
+
return json.load(f)
|