orzmc 2.0.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.
Files changed (41) hide show
  1. orzmc-2.0.0/.gitignore +177 -0
  2. orzmc-2.0.0/PKG-INFO +32 -0
  3. orzmc-2.0.0/README.md +14 -0
  4. orzmc-2.0.0/orzmc/__init__.py +171 -0
  5. orzmc-2.0.0/orzmc/core/client/__init__.py +21 -0
  6. orzmc-2.0.0/orzmc/core/client/base.py +85 -0
  7. orzmc-2.0.0/orzmc/core/client/fabric.py +28 -0
  8. orzmc-2.0.0/orzmc/core/client/forge.py +128 -0
  9. orzmc-2.0.0/orzmc/core/client/vanilla.py +17 -0
  10. orzmc-2.0.0/orzmc/core/fabric.py +97 -0
  11. orzmc-2.0.0/orzmc/core/forge.py +44 -0
  12. orzmc-2.0.0/orzmc/core/mojang.py +123 -0
  13. orzmc-2.0.0/orzmc/core/profiles.py +24 -0
  14. orzmc-2.0.0/orzmc/core/server/__init__.py +23 -0
  15. orzmc-2.0.0/orzmc/core/server/base.py +80 -0
  16. orzmc-2.0.0/orzmc/core/server/fabric.py +79 -0
  17. orzmc-2.0.0/orzmc/core/server/forge.py +85 -0
  18. orzmc-2.0.0/orzmc/core/server/paper.py +75 -0
  19. orzmc-2.0.0/orzmc/core/server/vanilla.py +26 -0
  20. orzmc-2.0.0/orzmc/domain/java.py +24 -0
  21. orzmc-2.0.0/orzmc/domain/launch.py +209 -0
  22. orzmc-2.0.0/orzmc/domain/libraries.py +157 -0
  23. orzmc-2.0.0/orzmc/domain/options.py +35 -0
  24. orzmc-2.0.0/orzmc/domain/paths.py +146 -0
  25. orzmc-2.0.0/orzmc/domain/types.py +44 -0
  26. orzmc-2.0.0/orzmc/infra/fs.py +75 -0
  27. orzmc-2.0.0/orzmc/infra/hashing.py +17 -0
  28. orzmc-2.0.0/orzmc/infra/http.py +78 -0
  29. orzmc-2.0.0/orzmc/infra/log.py +91 -0
  30. orzmc-2.0.0/orzmc/infra/progress.py +79 -0
  31. orzmc-2.0.0/orzmc/infra/runner.py +142 -0
  32. orzmc-2.0.0/orzmc/py.typed +0 -0
  33. orzmc-2.0.0/orzmc/services/backup.py +39 -0
  34. orzmc-2.0.0/orzmc/services/client.py +167 -0
  35. orzmc-2.0.0/orzmc/services/context.py +82 -0
  36. orzmc-2.0.0/orzmc/services/downloader.py +171 -0
  37. orzmc-2.0.0/orzmc/services/java.py +131 -0
  38. orzmc-2.0.0/orzmc/services/server.py +139 -0
  39. orzmc-2.0.0/orzmc/services/versions.py +80 -0
  40. orzmc-2.0.0/orzmc/version.py +3 -0
  41. orzmc-2.0.0/pyproject.toml +34 -0
orzmc-2.0.0/.gitignore ADDED
@@ -0,0 +1,177 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
163
+
164
+ .DS_Store
165
+ .vscode/
166
+
167
+ # uv
168
+ .venv/
169
+
170
+ # pytest
171
+ .pytest_cache/
172
+
173
+ # PyInstaller build dir (scripts/build.py)
174
+ .pybuild/
175
+
176
+ # runtime artifacts (managed under game root, not repo)
177
+ java/
orzmc-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: orzmc
3
+ Version: 2.0.0
4
+ Summary: OrzMC core library: bootstrap Minecraft client/server, manage a sandboxed Java runtime and multiple game versions
5
+ Author: OrzMC
6
+ License: MIT
7
+ Keywords: cli,java,launcher,minecraft,server
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Games/Entertainment
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: requests>=2.31
16
+ Requires-Dist: rich>=13.7
17
+ Description-Content-Type: text/markdown
18
+
19
+ # orzmc
20
+
21
+ OrzMC core library: bootstrap Minecraft client / server, manage a sandboxed Java
22
+ runtime and multiple game versions. Framework-free, reusable, independently
23
+ published to PyPI.
24
+
25
+ ```python
26
+ from orzmc import GameType, RuntimeOptions, launch_client
27
+
28
+ opts = RuntimeOptions(is_client=True, version="1.20.4", game_type="vanilla")
29
+ launch_client(opts)
30
+ ```
31
+
32
+ See the [project README](../README.md) for full documentation.
orzmc-2.0.0/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # orzmc
2
+
3
+ OrzMC core library: bootstrap Minecraft client / server, manage a sandboxed Java
4
+ runtime and multiple game versions. Framework-free, reusable, independently
5
+ published to PyPI.
6
+
7
+ ```python
8
+ from orzmc import GameType, RuntimeOptions, launch_client
9
+
10
+ opts = RuntimeOptions(is_client=True, version="1.20.4", game_type="vanilla")
11
+ launch_client(opts)
12
+ ```
13
+
14
+ See the [project README](../README.md) for full documentation.
@@ -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())