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.
- orzmc-2.0.0/.gitignore +177 -0
- orzmc-2.0.0/PKG-INFO +32 -0
- orzmc-2.0.0/README.md +14 -0
- orzmc-2.0.0/orzmc/__init__.py +171 -0
- orzmc-2.0.0/orzmc/core/client/__init__.py +21 -0
- orzmc-2.0.0/orzmc/core/client/base.py +85 -0
- orzmc-2.0.0/orzmc/core/client/fabric.py +28 -0
- orzmc-2.0.0/orzmc/core/client/forge.py +128 -0
- orzmc-2.0.0/orzmc/core/client/vanilla.py +17 -0
- orzmc-2.0.0/orzmc/core/fabric.py +97 -0
- orzmc-2.0.0/orzmc/core/forge.py +44 -0
- orzmc-2.0.0/orzmc/core/mojang.py +123 -0
- orzmc-2.0.0/orzmc/core/profiles.py +24 -0
- orzmc-2.0.0/orzmc/core/server/__init__.py +23 -0
- orzmc-2.0.0/orzmc/core/server/base.py +80 -0
- orzmc-2.0.0/orzmc/core/server/fabric.py +79 -0
- orzmc-2.0.0/orzmc/core/server/forge.py +85 -0
- orzmc-2.0.0/orzmc/core/server/paper.py +75 -0
- orzmc-2.0.0/orzmc/core/server/vanilla.py +26 -0
- orzmc-2.0.0/orzmc/domain/java.py +24 -0
- orzmc-2.0.0/orzmc/domain/launch.py +209 -0
- orzmc-2.0.0/orzmc/domain/libraries.py +157 -0
- orzmc-2.0.0/orzmc/domain/options.py +35 -0
- orzmc-2.0.0/orzmc/domain/paths.py +146 -0
- orzmc-2.0.0/orzmc/domain/types.py +44 -0
- orzmc-2.0.0/orzmc/infra/fs.py +75 -0
- orzmc-2.0.0/orzmc/infra/hashing.py +17 -0
- orzmc-2.0.0/orzmc/infra/http.py +78 -0
- orzmc-2.0.0/orzmc/infra/log.py +91 -0
- orzmc-2.0.0/orzmc/infra/progress.py +79 -0
- orzmc-2.0.0/orzmc/infra/runner.py +142 -0
- orzmc-2.0.0/orzmc/py.typed +0 -0
- orzmc-2.0.0/orzmc/services/backup.py +39 -0
- orzmc-2.0.0/orzmc/services/client.py +167 -0
- orzmc-2.0.0/orzmc/services/context.py +82 -0
- orzmc-2.0.0/orzmc/services/downloader.py +171 -0
- orzmc-2.0.0/orzmc/services/java.py +131 -0
- orzmc-2.0.0/orzmc/services/server.py +139 -0
- orzmc-2.0.0/orzmc/services/versions.py +80 -0
- orzmc-2.0.0/orzmc/version.py +3 -0
- 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())
|