modsmith-cli 0.1.1__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.
- modsmith_cli/__init__.py +8 -0
- modsmith_cli/__main__.py +6 -0
- modsmith_cli/app.py +224 -0
- modsmith_cli/commands/__init__.py +1 -0
- modsmith_cli/commands/config.py +81 -0
- modsmith_cli/commands/contents.py +332 -0
- modsmith_cli/commands/discovery.py +152 -0
- modsmith_cli/commands/install.py +351 -0
- modsmith_cli/commands/instance.py +234 -0
- modsmith_cli/commands/list.py +48 -0
- modsmith_cli/commands/search.py +205 -0
- modsmith_cli/commands/update.py +324 -0
- modsmith_cli/context.py +117 -0
- modsmith_cli/core/__init__.py +5 -0
- modsmith_cli/core/config.py +243 -0
- modsmith_cli/core/contents.py +68 -0
- modsmith_cli/core/errors.py +19 -0
- modsmith_cli/core/install.py +244 -0
- modsmith_cli/core/names.py +30 -0
- modsmith_cli/core/paths.py +63 -0
- modsmith_cli/core/platforms.py +98 -0
- modsmith_cli/core/records.py +73 -0
- modsmith_cli/core/refs.py +96 -0
- modsmith_cli/core/registry.py +183 -0
- modsmith_cli/errors.py +180 -0
- modsmith_cli/target.py +43 -0
- modsmith_cli/ui.py +219 -0
- modsmith_cli-0.1.1.dist-info/METADATA +255 -0
- modsmith_cli-0.1.1.dist-info/RECORD +32 -0
- modsmith_cli-0.1.1.dist-info/WHEEL +4 -0
- modsmith_cli-0.1.1.dist-info/entry_points.txt +2 -0
- modsmith_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Commands that look a project or a loader up, without changing anything."""
|
|
2
|
+
|
|
3
|
+
from itertools import islice
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from modsmith import loaders
|
|
8
|
+
from modsmith.errors import UnsupportedError
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from ..context import get
|
|
13
|
+
from ..core.errors import AppError
|
|
14
|
+
from ..core.platforms import explained
|
|
15
|
+
from ..core.refs import parse_project
|
|
16
|
+
from ..ui import show_changelog
|
|
17
|
+
from .install import LOADER_HINT, LOADER_NAMES
|
|
18
|
+
from .search import KINDS, Kind
|
|
19
|
+
|
|
20
|
+
RELEASES_SHOWN = 5
|
|
21
|
+
LOADERS_SHOWN = 15
|
|
22
|
+
CHANGELOG_LINES = 12
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _count(number: int) -> str:
|
|
26
|
+
return f"{number:,}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def info_command(
|
|
30
|
+
ctx: typer.Context,
|
|
31
|
+
project: Annotated[
|
|
32
|
+
str,
|
|
33
|
+
typer.Argument(
|
|
34
|
+
help="A name, platform:name, or the project's address.",
|
|
35
|
+
show_default=False,
|
|
36
|
+
),
|
|
37
|
+
],
|
|
38
|
+
kind: Annotated[
|
|
39
|
+
Kind, typer.Option("--type", "-t", help="The kind of project.")
|
|
40
|
+
] = Kind.MOD,
|
|
41
|
+
minecraft: Annotated[
|
|
42
|
+
str | None,
|
|
43
|
+
typer.Option("--minecraft", "-m", help="Only releases for this version."),
|
|
44
|
+
] = None,
|
|
45
|
+
loader: Annotated[
|
|
46
|
+
str | None,
|
|
47
|
+
typer.Option("--loader", "-l", help="Only releases for this loader."),
|
|
48
|
+
] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Show a project's details, its newest releases and what changed last."""
|
|
51
|
+
app = get(ctx)
|
|
52
|
+
ref = parse_project(project, app.config.default_platform)
|
|
53
|
+
with explained(ref.platform, [ref.name]), app.platform(ref.platform) as platform:
|
|
54
|
+
found = platform.get_project(ref.name, project_type=KINDS[kind])
|
|
55
|
+
authors = platform.authors(found)
|
|
56
|
+
releases = list(
|
|
57
|
+
islice(
|
|
58
|
+
platform.iter_releases(found, game_version=minecraft, loader=loader),
|
|
59
|
+
RELEASES_SHOWN,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
changelog = platform.changelog(releases[0]) if releases else ""
|
|
64
|
+
except UnsupportedError:
|
|
65
|
+
changelog = ""
|
|
66
|
+
|
|
67
|
+
app.out.print(
|
|
68
|
+
Text(found.name, style="bold"), Text(f"({ref.platform}:{found.slug})")
|
|
69
|
+
)
|
|
70
|
+
if found.summary:
|
|
71
|
+
app.out.print(found.summary)
|
|
72
|
+
facts = [
|
|
73
|
+
("By", ", ".join(authors)),
|
|
74
|
+
("Downloads", _count(found.downloads)),
|
|
75
|
+
("Categories", ", ".join(found.categories)),
|
|
76
|
+
("Minecraft", ", ".join(found.game_versions[:8])),
|
|
77
|
+
("Updated", found.updated[:10]),
|
|
78
|
+
("Page", found.url),
|
|
79
|
+
]
|
|
80
|
+
table = Table.grid(padding=(0, 2))
|
|
81
|
+
for label, value in facts:
|
|
82
|
+
if value:
|
|
83
|
+
table.add_row(Text(label, style="dim"), value)
|
|
84
|
+
app.out.print(table)
|
|
85
|
+
|
|
86
|
+
app.out.print()
|
|
87
|
+
if not releases:
|
|
88
|
+
app.out.print("No releases match.")
|
|
89
|
+
return
|
|
90
|
+
listed = Table(box=None, pad_edge=False, title_justify="left")
|
|
91
|
+
for column in ("Release", "Type", "Minecraft", "Loaders", "Published"):
|
|
92
|
+
listed.add_column(column, no_wrap=column != "Minecraft")
|
|
93
|
+
for release in releases:
|
|
94
|
+
listed.add_row(
|
|
95
|
+
release.version or release.name,
|
|
96
|
+
release.release_type,
|
|
97
|
+
", ".join(release.game_versions[:4]),
|
|
98
|
+
", ".join(release.loaders),
|
|
99
|
+
release.published[:10],
|
|
100
|
+
)
|
|
101
|
+
app.out.print(listed)
|
|
102
|
+
if changelog:
|
|
103
|
+
app.out.print()
|
|
104
|
+
show_changelog(
|
|
105
|
+
app,
|
|
106
|
+
f"What changed in {releases[0].name}",
|
|
107
|
+
changelog,
|
|
108
|
+
lines=CHANGELOG_LINES,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def loaders_command(
|
|
113
|
+
ctx: typer.Context,
|
|
114
|
+
kind: Annotated[
|
|
115
|
+
str,
|
|
116
|
+
typer.Argument(help="fabric, quilt, forge or neoforge.", show_default=False),
|
|
117
|
+
],
|
|
118
|
+
minecraft: Annotated[
|
|
119
|
+
str, typer.Argument(help="The Minecraft version.", show_default=False)
|
|
120
|
+
],
|
|
121
|
+
everything: Annotated[
|
|
122
|
+
bool, typer.Option("--all", help="List every version, not only the newest.")
|
|
123
|
+
] = False,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""List a mod loader's versions for a Minecraft version."""
|
|
126
|
+
app = get(ctx)
|
|
127
|
+
try:
|
|
128
|
+
found = loaders.versions(kind.lower(), minecraft)
|
|
129
|
+
except UnsupportedError as e:
|
|
130
|
+
raise AppError(f"Modsmith cannot install {kind!r}.", hint=LOADER_HINT) from e
|
|
131
|
+
name = LOADER_NAMES.get(kind.lower(), kind)
|
|
132
|
+
if not found:
|
|
133
|
+
app.out.print(f"{name} has no versions for Minecraft {minecraft}.")
|
|
134
|
+
return
|
|
135
|
+
shown = found if everything else found[:LOADERS_SHOWN]
|
|
136
|
+
table = Table(box=None, pad_edge=False)
|
|
137
|
+
table.add_column("Version", no_wrap=True)
|
|
138
|
+
table.add_column("")
|
|
139
|
+
for item in shown:
|
|
140
|
+
notes = ["recommended"] if item.recommended else []
|
|
141
|
+
if not item.stable:
|
|
142
|
+
notes.append("beta")
|
|
143
|
+
table.add_row(item.version, ", ".join(notes))
|
|
144
|
+
app.out.print(Text(f"{name} for Minecraft {minecraft}", style="bold"))
|
|
145
|
+
app.out.print(table)
|
|
146
|
+
if len(shown) < len(found):
|
|
147
|
+
app.out.print(
|
|
148
|
+
Text(
|
|
149
|
+
f"{len(found) - len(shown)} older versions; see them with --all.",
|
|
150
|
+
style="dim",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""``modsmith install`` and ``modsmith create``: new instances, ready to play."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from modsmith.errors import InstanceError, UnresolvedEntriesError, UnsupportedError
|
|
8
|
+
from modsmith.instances import Instance
|
|
9
|
+
from modsmith.loaders import Loader, create, recommended
|
|
10
|
+
from modsmith.minecraft import GameDir, install_version, list_versions
|
|
11
|
+
from modsmith.packs import Modpack
|
|
12
|
+
|
|
13
|
+
from ..context import AppContext, get
|
|
14
|
+
from ..core.config import Config
|
|
15
|
+
from ..core.errors import AppError
|
|
16
|
+
from ..core.install import (
|
|
17
|
+
check_launcher,
|
|
18
|
+
download_sizes,
|
|
19
|
+
fetch_pack,
|
|
20
|
+
match_optional,
|
|
21
|
+
optional_entries,
|
|
22
|
+
profile_key,
|
|
23
|
+
profile_name,
|
|
24
|
+
project_icon,
|
|
25
|
+
with_optional,
|
|
26
|
+
)
|
|
27
|
+
from ..core.names import check_name, name_from
|
|
28
|
+
from ..core.platforms import explained
|
|
29
|
+
from ..core.records import Record, write_record
|
|
30
|
+
from ..core.refs import ProjectRef, parse_pack
|
|
31
|
+
from ..errors import report as report_error
|
|
32
|
+
from ..target import DryRun, Yes
|
|
33
|
+
from ..ui import cancellable, confirm, progress, show_plan
|
|
34
|
+
|
|
35
|
+
LOADER_NAMES = {
|
|
36
|
+
"forge": "Forge",
|
|
37
|
+
"neoforge": "NeoForge",
|
|
38
|
+
"fabric": "Fabric",
|
|
39
|
+
"quilt": "Quilt",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def loader_label(kind: str | None, version: str | None) -> str:
|
|
44
|
+
"""Name a loader for people, such as ``Fabric 0.16.10``."""
|
|
45
|
+
if not kind:
|
|
46
|
+
return "no mod loader"
|
|
47
|
+
name = LOADER_NAMES.get(kind.lower(), kind)
|
|
48
|
+
return f"{name} {version}" if version else name
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def free_folder(name: str, config: Config) -> Path:
|
|
52
|
+
"""Return the folder for a new instance.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
AppError: An instance or other files are already there.
|
|
56
|
+
"""
|
|
57
|
+
path = config.instances_dir / check_name(name)
|
|
58
|
+
try:
|
|
59
|
+
taken = Instance(path).state is not None
|
|
60
|
+
except InstanceError:
|
|
61
|
+
taken = True
|
|
62
|
+
if taken:
|
|
63
|
+
raise AppError(
|
|
64
|
+
f"There is already an instance called {name!r}.",
|
|
65
|
+
hint=f"Update it with: modsmith update {name}. Or choose another "
|
|
66
|
+
"name with --name.",
|
|
67
|
+
)
|
|
68
|
+
if path.exists() and any(path.iterdir()):
|
|
69
|
+
raise AppError(
|
|
70
|
+
f"{path} already holds other files.",
|
|
71
|
+
hint="Choose another name with --name.",
|
|
72
|
+
)
|
|
73
|
+
return path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ask_optional(app: AppContext, pack: Modpack) -> list[str]:
|
|
77
|
+
"""Let the player pick optional files by number."""
|
|
78
|
+
options = [entry.path for entry in optional_entries(pack) if entry.path]
|
|
79
|
+
app.out.print("This pack has optional files:")
|
|
80
|
+
for index, path in enumerate(options, 1):
|
|
81
|
+
app.out.print(f" {index}. {path}")
|
|
82
|
+
answer = typer.prompt(
|
|
83
|
+
"Which do you want? Numbers separated by spaces, all, or Enter for none",
|
|
84
|
+
default="",
|
|
85
|
+
show_default=False,
|
|
86
|
+
).strip()
|
|
87
|
+
if answer.lower() == "all":
|
|
88
|
+
return options
|
|
89
|
+
try:
|
|
90
|
+
picked = [int(word) for word in answer.replace(",", " ").split()]
|
|
91
|
+
except ValueError:
|
|
92
|
+
raise AppError(f"{answer!r} is not a list of numbers.") from None
|
|
93
|
+
if any(number < 1 or number > len(options) for number in picked):
|
|
94
|
+
raise AppError(f"Choose numbers from 1 to {len(options)}.")
|
|
95
|
+
return [options[number - 1] for number in dict.fromkeys(picked)]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def choose_optional(
|
|
99
|
+
app: AppContext, pack: Modpack, patterns: list[str], everything: bool, yes: bool
|
|
100
|
+
) -> list[str]:
|
|
101
|
+
"""Decide which optional files to install, asking if nobody said."""
|
|
102
|
+
options = optional_entries(pack)
|
|
103
|
+
if everything:
|
|
104
|
+
return [entry.path for entry in options if entry.path]
|
|
105
|
+
if patterns:
|
|
106
|
+
return match_optional(pack, patterns)
|
|
107
|
+
if not options:
|
|
108
|
+
return []
|
|
109
|
+
if yes or not app.services.interactive():
|
|
110
|
+
app.err.print(
|
|
111
|
+
f"Left out {len(options)} optional files. "
|
|
112
|
+
"Choose them with --optional or --all-optional.",
|
|
113
|
+
style="dim",
|
|
114
|
+
)
|
|
115
|
+
return []
|
|
116
|
+
return _ask_optional(app, pack)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Optional = Annotated[
|
|
120
|
+
list[str] | None,
|
|
121
|
+
typer.Option(
|
|
122
|
+
"--optional",
|
|
123
|
+
help="Install the optional files whose names contain this. Repeat for more.",
|
|
124
|
+
show_default=False,
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
AllOptional = Annotated[
|
|
128
|
+
bool, typer.Option("--all-optional", help="Install every optional file.")
|
|
129
|
+
]
|
|
130
|
+
From = Annotated[
|
|
131
|
+
list[Path] | None,
|
|
132
|
+
typer.Option(
|
|
133
|
+
"--from",
|
|
134
|
+
help="A folder with files you downloaded by hand. Repeat for more.",
|
|
135
|
+
show_default=False,
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
Overwrite = Annotated[
|
|
139
|
+
bool,
|
|
140
|
+
typer.Option(
|
|
141
|
+
"--overwrite", help="Replace files you changed, instead of keeping them."
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def install_command(
|
|
147
|
+
ctx: typer.Context,
|
|
148
|
+
source: Annotated[
|
|
149
|
+
str,
|
|
150
|
+
typer.Argument(
|
|
151
|
+
help="A pack file, a project such as fabulously-optimized or "
|
|
152
|
+
"curseforge:<id>, or its web address.",
|
|
153
|
+
show_default=False,
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
name: Annotated[
|
|
157
|
+
str | None,
|
|
158
|
+
typer.Option("--name", help="The instance's name. Defaults to the pack's."),
|
|
159
|
+
] = None,
|
|
160
|
+
optional: Optional = None,
|
|
161
|
+
all_optional: AllOptional = False,
|
|
162
|
+
from_folders: From = None,
|
|
163
|
+
yes: Yes = False,
|
|
164
|
+
dry_run: DryRun = False,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Install a modpack as a new instance in the official launcher."""
|
|
167
|
+
app = get(ctx)
|
|
168
|
+
config = app.config
|
|
169
|
+
ref = parse_pack(source, config.default_platform)
|
|
170
|
+
game = GameDir(config.game_dir)
|
|
171
|
+
launcher = (
|
|
172
|
+
None if dry_run else check_launcher(game, app.services.launcher_running())
|
|
173
|
+
)
|
|
174
|
+
platform, wanted = (
|
|
175
|
+
(ref.platform, [ref.name]) if isinstance(ref, ProjectRef) else ("", [])
|
|
176
|
+
)
|
|
177
|
+
with cancellable() as token, progress(app) as report, explained(platform, wanted):
|
|
178
|
+
fetched = fetch_pack(
|
|
179
|
+
ref,
|
|
180
|
+
app.platform,
|
|
181
|
+
cache_dir=app.paths.cache_dir,
|
|
182
|
+
allow_prereleases=config.allow_prereleases,
|
|
183
|
+
progress=report,
|
|
184
|
+
cancel=token,
|
|
185
|
+
)
|
|
186
|
+
path = free_folder(name or name_from(fetched.pack.name), config)
|
|
187
|
+
chosen = choose_optional(app, fetched.pack, optional or [], all_optional, yes)
|
|
188
|
+
pack = with_optional(fetched.pack, chosen)
|
|
189
|
+
missing = [entry for entry in pack.unresolved if entry.wanted("client")]
|
|
190
|
+
if missing and not from_folders and not dry_run:
|
|
191
|
+
raise UnresolvedEntriesError(missing)
|
|
192
|
+
|
|
193
|
+
instance = Instance(path)
|
|
194
|
+
title = f"Install {pack.name} {pack.version} as {path.name}"
|
|
195
|
+
show_plan(app, instance.diff(pack), download_sizes(pack), title=title)
|
|
196
|
+
app.out.print(
|
|
197
|
+
f"Minecraft {pack.minecraft} with "
|
|
198
|
+
f"{loader_label(pack.loader_kind, pack.loader_version)}, "
|
|
199
|
+
"and an installation in the official launcher."
|
|
200
|
+
)
|
|
201
|
+
if missing and not from_folders:
|
|
202
|
+
report_error(
|
|
203
|
+
UnresolvedEntriesError(missing), app.err, prefix="Before you install: "
|
|
204
|
+
)
|
|
205
|
+
if dry_run or launcher is None:
|
|
206
|
+
return
|
|
207
|
+
confirm(app, "Install it?", yes=yes)
|
|
208
|
+
with cancellable() as token, progress(app) as report:
|
|
209
|
+
version_id = pack.install_game(
|
|
210
|
+
game, cache_dir=app.paths.cache_dir, progress=report, cancel=token
|
|
211
|
+
)
|
|
212
|
+
instance.sync(
|
|
213
|
+
pack, adopt_from=from_folders or (), progress=report, cancel=token
|
|
214
|
+
)
|
|
215
|
+
key = profile_key(path.name)
|
|
216
|
+
shown = profile_name(launcher, pack.name, key, path.name)
|
|
217
|
+
key = launcher.add_profile(
|
|
218
|
+
shown,
|
|
219
|
+
version_id,
|
|
220
|
+
instance.path,
|
|
221
|
+
key=key,
|
|
222
|
+
memory_mb=pack.memory_mb,
|
|
223
|
+
icon=project_icon(fetched.project),
|
|
224
|
+
)
|
|
225
|
+
write_record(instance.path, Record(fetched.source, key, tuple(chosen)))
|
|
226
|
+
app.out.print(
|
|
227
|
+
f"Installed {pack.name} as {path.name}. In the official launcher, "
|
|
228
|
+
f"choose the {shown} installation and play."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
LOADER_HINT = "Use fabric, quilt, forge or neoforge."
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _registered(kind: str, minecraft: str) -> bool:
|
|
236
|
+
"""Return whether the library knows a loader by this name."""
|
|
237
|
+
try:
|
|
238
|
+
create(kind, "0", minecraft)
|
|
239
|
+
except UnsupportedError:
|
|
240
|
+
return False
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _pick_loader(
|
|
245
|
+
kind: str | None, version: str | None, minecraft: str
|
|
246
|
+
) -> Loader | None:
|
|
247
|
+
"""Return the loader to install, choosing a version if none was given.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
AppError: The loader is unknown, or has no version for this Minecraft.
|
|
251
|
+
"""
|
|
252
|
+
if version and not kind:
|
|
253
|
+
raise AppError("--loader-version needs --loader too.")
|
|
254
|
+
if not kind:
|
|
255
|
+
return None
|
|
256
|
+
try:
|
|
257
|
+
if version:
|
|
258
|
+
return create(kind, version, minecraft)
|
|
259
|
+
return recommended(kind, minecraft)
|
|
260
|
+
except UnsupportedError as e:
|
|
261
|
+
if not _registered(kind, minecraft):
|
|
262
|
+
raise AppError(
|
|
263
|
+
f"Modsmith cannot install {kind!r}.", hint=LOADER_HINT
|
|
264
|
+
) from e
|
|
265
|
+
raise AppError(
|
|
266
|
+
f"{loader_label(kind, None)} has no version for Minecraft {minecraft}.",
|
|
267
|
+
hint="Check the Minecraft version, or choose another loader.",
|
|
268
|
+
) from e
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _check_minecraft(game: GameDir, minecraft: str) -> None:
|
|
272
|
+
"""Make sure a Minecraft version exists before planning around it.
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
AppError: Mojang has no such version.
|
|
276
|
+
"""
|
|
277
|
+
if game.has_version(minecraft):
|
|
278
|
+
return
|
|
279
|
+
if not any(item.id == minecraft for item in list_versions()):
|
|
280
|
+
raise AppError(
|
|
281
|
+
f"There is no Minecraft {minecraft}.",
|
|
282
|
+
hint="Use a version such as 1.21.1. Snapshots work too.",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def create_command(
|
|
287
|
+
ctx: typer.Context,
|
|
288
|
+
name: Annotated[
|
|
289
|
+
str, typer.Argument(help="The new instance's name.", show_default=False)
|
|
290
|
+
],
|
|
291
|
+
minecraft: Annotated[
|
|
292
|
+
str,
|
|
293
|
+
typer.Option(
|
|
294
|
+
"--minecraft", "-m", help="The Minecraft version.", show_default=False
|
|
295
|
+
),
|
|
296
|
+
],
|
|
297
|
+
loader: Annotated[
|
|
298
|
+
str | None,
|
|
299
|
+
typer.Option("--loader", "-l", help="fabric, quilt, forge or neoforge."),
|
|
300
|
+
] = None,
|
|
301
|
+
loader_version: Annotated[
|
|
302
|
+
str | None,
|
|
303
|
+
typer.Option("--loader-version", help="Defaults to the recommended version."),
|
|
304
|
+
] = None,
|
|
305
|
+
yes: Yes = False,
|
|
306
|
+
dry_run: DryRun = False,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Create an empty instance, with or without a mod loader."""
|
|
309
|
+
app = get(ctx)
|
|
310
|
+
config = app.config
|
|
311
|
+
path = free_folder(name, config)
|
|
312
|
+
game = GameDir(config.game_dir)
|
|
313
|
+
launcher = (
|
|
314
|
+
None if dry_run else check_launcher(game, app.services.launcher_running())
|
|
315
|
+
)
|
|
316
|
+
chosen = _pick_loader(loader, loader_version, minecraft)
|
|
317
|
+
if chosen is None:
|
|
318
|
+
_check_minecraft(game, minecraft)
|
|
319
|
+
label = loader_label(
|
|
320
|
+
chosen.kind if chosen else None, chosen.version if chosen else None
|
|
321
|
+
)
|
|
322
|
+
app.out.print(
|
|
323
|
+
f"Create {name}: Minecraft {minecraft} with {label}, "
|
|
324
|
+
"and an installation in the official launcher."
|
|
325
|
+
)
|
|
326
|
+
if dry_run or launcher is None:
|
|
327
|
+
return
|
|
328
|
+
confirm(app, "Create it?", yes=yes)
|
|
329
|
+
with cancellable() as token, progress(app) as report:
|
|
330
|
+
if chosen is None:
|
|
331
|
+
version_id = install_version(
|
|
332
|
+
game, minecraft, progress=report, cancel=token
|
|
333
|
+
).id
|
|
334
|
+
else:
|
|
335
|
+
version_id = chosen.install(
|
|
336
|
+
game, cache_dir=app.paths.cache_dir, progress=report, cancel=token
|
|
337
|
+
)
|
|
338
|
+
Instance.create(
|
|
339
|
+
path,
|
|
340
|
+
minecraft=minecraft,
|
|
341
|
+
loader=chosen.kind if chosen else None,
|
|
342
|
+
loader_version=chosen.version if chosen else None,
|
|
343
|
+
)
|
|
344
|
+
key = profile_key(name)
|
|
345
|
+
key = launcher.add_profile(
|
|
346
|
+
profile_name(launcher, name, key, name), version_id, path, key=key
|
|
347
|
+
)
|
|
348
|
+
write_record(path, Record(profile=key))
|
|
349
|
+
app.out.print(
|
|
350
|
+
f"Created {name}. In the official launcher, choose the {name} installation."
|
|
351
|
+
)
|