mcserver-cli 3.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.
- mcserver/__init__.py +1 -0
- mcserver/__main__.py +3 -0
- mcserver/app.py +24 -0
- mcserver/caching.py +0 -0
- mcserver/cli.py +326 -0
- mcserver/commands/__init__.py +39 -0
- mcserver/commands/add_projects.py +7 -0
- mcserver/commands/import_setup.py +6 -0
- mcserver/commands/initialize_server.py +119 -0
- mcserver/commands/list_projects.py +6 -0
- mcserver/commands/op_grant.py +7 -0
- mcserver/commands/op_list.py +32 -0
- mcserver/commands/op_revoke.py +7 -0
- mcserver/commands/print_help.py +7 -0
- mcserver/commands/remove_projects.py +6 -0
- mcserver/commands/search_projects.py +7 -0
- mcserver/commands/server_status.py +6 -0
- mcserver/commands/show_projects.py +26 -0
- mcserver/commands/start_server.py +150 -0
- mcserver/commands/stop_server.py +46 -0
- mcserver/commands/update_projects.py +6 -0
- mcserver/commands/whitelist_add.py +7 -0
- mcserver/commands/whitelist_list.py +31 -0
- mcserver/commands/whitelist_remove.py +31 -0
- mcserver/commandsa.py +1297 -0
- mcserver/config.py +37 -0
- mcserver/constants.py +14 -0
- mcserver/fabricmc/meta.py +41 -0
- mcserver/legacyfabric/meta.py +37 -0
- mcserver/metadata.py +33 -0
- mcserver/minecraft/api.py +94 -0
- mcserver/minecraft/player_identity.py +21 -0
- mcserver/modrinth/api.py +657 -0
- mcserver/modrinth/modpack.py +101 -0
- mcserver/modrinth/project_records/metadata.py +0 -0
- mcserver/modrinth/project_records/relationships.py +203 -0
- mcserver/modrinth/project_records/shared.py +9 -0
- mcserver/modrinth/projects.py +80 -0
- mcserver/modrinth/shared.py +13 -0
- mcserver/modrinth/slug_index.py +36 -0
- mcserver/mojang/eula.py +28 -0
- mcserver/mojang/manifest.py +69 -0
- mcserver/mysterious.py +5 -0
- mcserver/networking.py +152 -0
- mcserver/papermc/api.py +81 -0
- mcserver/purpurmc/api.py +81 -0
- mcserver/quiltmc/meta.py +54 -0
- mcserver/resolver.py +164 -0
- mcserver/server_files/banned_ips.py +120 -0
- mcserver/server_files/banned_players.py +166 -0
- mcserver/server_files/ops.py +143 -0
- mcserver/server_files/properties.py +41 -0
- mcserver/server_files/usercaches.py +0 -0
- mcserver/server_files/whitelist.py +163 -0
- mcserver/shared.py +198 -0
- mcserver/state.py +77 -0
- mcserver_cli-3.0.0.dist-info/METADATA +63 -0
- mcserver_cli-3.0.0.dist-info/RECORD +61 -0
- mcserver_cli-3.0.0.dist-info/WHEEL +4 -0
- mcserver_cli-3.0.0.dist-info/entry_points.txt +2 -0
- mcserver_cli-3.0.0.dist-info/licenses/LICENSE +9 -0
mcserver/commandsa.py
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
# Modules
|
|
2
|
+
# Standard
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging as log
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import textwrap
|
|
11
|
+
from collections import deque
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import config, networking, state
|
|
15
|
+
|
|
16
|
+
# MCServer
|
|
17
|
+
from .config import default_configs, generate_config, load_config
|
|
18
|
+
from .constants import __version__
|
|
19
|
+
from .fabricmc import meta as fabricmc_meta
|
|
20
|
+
|
|
21
|
+
# from indexing import project_index_exists, create_project_index, read_project_index, slug_to_id, slug_id_file, slug_id, indexes_dir, update_project_index
|
|
22
|
+
from .metadata import metadata_file
|
|
23
|
+
from .modrinth import modpack as modrinth_modpack
|
|
24
|
+
from .modrinth.api import VersionDependency, get_project_versions, modrinth_base_api
|
|
25
|
+
from .mojang import eula as mojang_eula
|
|
26
|
+
from .mojang import manifest as mojang_manifest
|
|
27
|
+
from .networking import download, request
|
|
28
|
+
from .papermc import api as papermc_api
|
|
29
|
+
from .purpurmc import api as purpurmc_api
|
|
30
|
+
from .server_files import whitelist as server_whitelist
|
|
31
|
+
from .shared import (
|
|
32
|
+
ansi,
|
|
33
|
+
confirmation_prompt,
|
|
34
|
+
format_number,
|
|
35
|
+
mcserver_dir,
|
|
36
|
+
mod_environment_color,
|
|
37
|
+
pluralize,
|
|
38
|
+
wrap_ansi,
|
|
39
|
+
)
|
|
40
|
+
from .state import get_state, is_active, set_state
|
|
41
|
+
|
|
42
|
+
# Variables
|
|
43
|
+
debug_mode = os.getenv("MCSERVER_DEBUG") == "1"
|
|
44
|
+
log_level = log.DEBUG if debug_mode else log.INFO
|
|
45
|
+
|
|
46
|
+
mods_loader = ["fabric"]
|
|
47
|
+
plugins_loader = ["paper", "purpur"]
|
|
48
|
+
loaders_list = ["vanilla"] + mods_loader + plugins_loader
|
|
49
|
+
|
|
50
|
+
eula_agree_sentence = "Yes, I agree."
|
|
51
|
+
|
|
52
|
+
# Severity levels
|
|
53
|
+
version_dependency_types = {
|
|
54
|
+
"embedded": 0,
|
|
55
|
+
"optional": 1,
|
|
56
|
+
"required": 2,
|
|
57
|
+
"incompatible": 3,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Paths
|
|
61
|
+
mods_dir = Path("mods")
|
|
62
|
+
plugins_dir = Path("plugins")
|
|
63
|
+
|
|
64
|
+
configs_dir = mcserver_dir / "configs"
|
|
65
|
+
tempfiles_dir = mcserver_dir / "tempfiles"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Error Classes
|
|
69
|
+
class AddProjectsError(Exception):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class InitializeServerError(Exception):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InstallServerError(Exception):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SearchProjectsError(Exception):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ShowProjectsError(Exception):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EULAAgreementError(Exception):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FetchProjectVersionError(Exception):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DownloadURLError(Exception):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ResolveProjectsConflictsError(Exception):
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
project_id: str,
|
|
105
|
+
incompatible_dependants: list | str,
|
|
106
|
+
required_dependants: list | str | None = None,
|
|
107
|
+
):
|
|
108
|
+
if required_dependants is None:
|
|
109
|
+
required_dependants = []
|
|
110
|
+
if isinstance(required_dependants, str):
|
|
111
|
+
required_dependants = [required_dependants]
|
|
112
|
+
if isinstance(incompatible_dependants, str):
|
|
113
|
+
incompatible_dependants = [incompatible_dependants]
|
|
114
|
+
|
|
115
|
+
required_join = "', '".join(required_dependants)
|
|
116
|
+
required_message = (
|
|
117
|
+
f" but required by '{required_join}'"
|
|
118
|
+
if required_dependants
|
|
119
|
+
else " but it is requested"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
incompatible_join = "', '".join(incompatible_dependants)
|
|
123
|
+
super().__init__(
|
|
124
|
+
f"Project '{project_id}' is incompatible with '{incompatible_join}'{required_message}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.project_id = project_id
|
|
128
|
+
|
|
129
|
+
self.incompatible_dependants = incompatible_dependants
|
|
130
|
+
self.required_dependants = required_dependants
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Functions
|
|
134
|
+
def print_help(args):
|
|
135
|
+
args.parser.print_help()
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def hahaha_yes():
|
|
140
|
+
print("The cake is actually real.")
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def wrap_string(
|
|
145
|
+
string: str,
|
|
146
|
+
initial_indent: str | int = "",
|
|
147
|
+
subsequent_indent: str | int = "",
|
|
148
|
+
width: int | None = None,
|
|
149
|
+
):
|
|
150
|
+
if isinstance(initial_indent, int):
|
|
151
|
+
initial_indent = " " * initial_indent
|
|
152
|
+
|
|
153
|
+
if isinstance(subsequent_indent, int):
|
|
154
|
+
subsequent_indent = " " * subsequent_indent
|
|
155
|
+
|
|
156
|
+
terminal_width = width or shutil.get_terminal_size().columns
|
|
157
|
+
return "\n".join(
|
|
158
|
+
textwrap.wrap(
|
|
159
|
+
string,
|
|
160
|
+
width=terminal_width,
|
|
161
|
+
initial_indent=initial_indent,
|
|
162
|
+
subsequent_indent=subsequent_indent,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
project_label = "project"
|
|
168
|
+
loader_dir = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def set_loader_context(loader_name: str):
|
|
172
|
+
global project_label
|
|
173
|
+
global loader_dir
|
|
174
|
+
|
|
175
|
+
if loader_name in mods_loader:
|
|
176
|
+
project_label = "mod"
|
|
177
|
+
loader_dir = mods_dir
|
|
178
|
+
elif loader_name in plugins_loader:
|
|
179
|
+
project_label = "plugin"
|
|
180
|
+
loader_dir = plugins_dir
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Resolve prejects dependencies
|
|
184
|
+
def adapt_dependencies_data(dependencies: list[VersionDependency]) -> dict:
|
|
185
|
+
dependencies_data = {}
|
|
186
|
+
for dependency in dependencies:
|
|
187
|
+
if "project_id" in dependency:
|
|
188
|
+
if (
|
|
189
|
+
dependency["project_id"] is not None
|
|
190
|
+
and dependency["dependency_type"]
|
|
191
|
+
!= version_dependency_types["embedded"]
|
|
192
|
+
):
|
|
193
|
+
dependencies_data[dependency["project_id"]] = version_dependency_types[
|
|
194
|
+
dependency["dependency_type"]
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
return dependencies_data
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def filter_dependencies_type(dependencies: dict, filter_type: str):
|
|
201
|
+
dependencies_list = []
|
|
202
|
+
for dependency_id, dependency_type in dependencies.items():
|
|
203
|
+
if dependency_type == version_dependency_types[filter_type]:
|
|
204
|
+
dependencies_list.append(dependency_id)
|
|
205
|
+
|
|
206
|
+
return dependencies_list
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# def resolve_projects(projects_id: list | str, game_version: str, loader_name: str) -> dict:
|
|
210
|
+
# projects_id = list(projects_id)
|
|
211
|
+
# unresolved_ids = deque()
|
|
212
|
+
# resolved_data = {}
|
|
213
|
+
|
|
214
|
+
# # Initial seeding
|
|
215
|
+
# for project_id in projects_id:
|
|
216
|
+
# project_id = slug_to_id(project_id)
|
|
217
|
+
|
|
218
|
+
# log.debug(f"Adding project '{project_id}' as unresolved")
|
|
219
|
+
# unresolved_ids.append(project_id)
|
|
220
|
+
# resolved_data[project_id] = {
|
|
221
|
+
# "metadata": {},
|
|
222
|
+
# "relationship": {
|
|
223
|
+
# "manual": True,
|
|
224
|
+
# "type": version_dependency_types["required"],
|
|
225
|
+
# "dependencies": {},
|
|
226
|
+
# "dependants": {}
|
|
227
|
+
# }
|
|
228
|
+
# }
|
|
229
|
+
|
|
230
|
+
# # Main code
|
|
231
|
+
# while unresolved_ids:
|
|
232
|
+
# project_id = unresolved_ids.popleft()
|
|
233
|
+
|
|
234
|
+
# project_data = resolved_data.pop(project_id)
|
|
235
|
+
# project_metadata = project_data["metadata"]
|
|
236
|
+
# project_relationship = project_data["relationship"]
|
|
237
|
+
|
|
238
|
+
# log.debug(f"Processing project '{project_id}'")
|
|
239
|
+
|
|
240
|
+
# skip_fetch_version = False
|
|
241
|
+
|
|
242
|
+
# if project_index_exists(project_id):
|
|
243
|
+
# project_index_data = read_project_index(project_id)
|
|
244
|
+
|
|
245
|
+
# project_index_relationship = project_index_data["relationship"]
|
|
246
|
+
# project_dependency_type = project_index_relationship["type"]
|
|
247
|
+
|
|
248
|
+
# if project_dependency_type == version_dependency_types["incompatible"]:
|
|
249
|
+
# project_dependants = project_index_relationship["dependants"]
|
|
250
|
+
|
|
251
|
+
# incompatible_dependants = filter_dependencies_type(project_dependants, "incompatible")
|
|
252
|
+
# required_dependants = filter_dependencies_type(project_dependants, "required")
|
|
253
|
+
# raise ResolveProjectsConflictsError(project_id, incompatible_dependants, required_dependants)
|
|
254
|
+
|
|
255
|
+
# log.debug("Project index exists, using it as cache")
|
|
256
|
+
# project_index_relationship["dependants"].update(project_relationship["dependants"])
|
|
257
|
+
# if project_relationship["manual"]:
|
|
258
|
+
# project_index_relationship["manual"] = True
|
|
259
|
+
|
|
260
|
+
# project_data = project_index_data
|
|
261
|
+
# skip_fetch_version = True
|
|
262
|
+
|
|
263
|
+
# if not skip_fetch_version:
|
|
264
|
+
# log.debug("Fetching project version")
|
|
265
|
+
# version = get_project_versions(project_id, game_version, loader_name)[0]
|
|
266
|
+
# project_id = version["project_id"]
|
|
267
|
+
|
|
268
|
+
# # Check if entry with project ID still exists (meaning we're still using slug)
|
|
269
|
+
# if project_id in resolved_data:
|
|
270
|
+
# log.debug(f"Found existing data with ID {project_id}")
|
|
271
|
+
# existing_entry = resolved_data.pop(project_id)
|
|
272
|
+
# existing_entry_relationship = existing_entry["relationship"]
|
|
273
|
+
|
|
274
|
+
# # Check if existing entry is marked as incompatible
|
|
275
|
+
# if existing_entry_relationship["type"] == version_dependency_types["incompatible"]:
|
|
276
|
+
# incompatible_dependants = filter_dependencies_type(existing_entry_relationship["dependants"], "incompatible")
|
|
277
|
+
# raise ResolveProjectsConflictsError(project_id, incompatible_dependants)
|
|
278
|
+
|
|
279
|
+
# log.debug(f"Keeping existing data")
|
|
280
|
+
# project_relationship["dependants"].update(existing_entry_relationship["dependants"])
|
|
281
|
+
|
|
282
|
+
# if "version_id" in version:
|
|
283
|
+
# project_metadata["version_id"] = version["version_id"]
|
|
284
|
+
|
|
285
|
+
# if "version_name" in version:
|
|
286
|
+
# project_metadata["version_name"] = version["version_name"]
|
|
287
|
+
|
|
288
|
+
# if "version_number" in version:
|
|
289
|
+
# project_metadata["version_number"] = version["version_number"]
|
|
290
|
+
|
|
291
|
+
# if "dependencies" in version:
|
|
292
|
+
# project_relationship["dependencies"] = adapt_dependencies_data(version["dependencies"])
|
|
293
|
+
|
|
294
|
+
# for dependency_id, dependency_type in project_relationship["dependencies"].items():
|
|
295
|
+
# if dependency_id in resolved_data:
|
|
296
|
+
# dependency_data = resolved_data[dependency_id]
|
|
297
|
+
# dependency_data_relationship = dependency_data["relationship"]
|
|
298
|
+
|
|
299
|
+
# dependency_data_type = dependency_data_relationship["type"]
|
|
300
|
+
# dependency_data_dependants = dependency_data_relationship["dependants"]
|
|
301
|
+
|
|
302
|
+
# if dependency_type == version_dependency_types["required"]:
|
|
303
|
+
# if dependency_data_type == version_dependency_types["incompatible"]:
|
|
304
|
+
# incompatible_dependants = filter_dependencies_type(dependency_data_dependants, "incompatible")
|
|
305
|
+
# raise ResolveProjectsConflictsError(dependency_id, incompatible_dependants, project_id)
|
|
306
|
+
# elif dependency_data_type == version_dependency_types["optional"]:
|
|
307
|
+
# log.debug(f"Adding initially optional dependency '{project_id}' to unresolved")
|
|
308
|
+
# unresolved_ids.append(dependency_id)
|
|
309
|
+
# elif dependency_type == version_dependency_types["incompatible"] and dependency_data_type == version_dependency_types["required"]:
|
|
310
|
+
# required_dependants = filter_dependencies_type(dependency_data_dependants, "required")
|
|
311
|
+
# raise ResolveProjectsConflictsError(dependency_id, project_id, required_dependants)
|
|
312
|
+
|
|
313
|
+
# log.debug(f"Updating project data '{dependency_id}' with new '{dependency_type}' type dependant")
|
|
314
|
+
# dependency_data_dependants[project_id] = dependency_type
|
|
315
|
+
# dependency_data_relationship["type"] = max(dependency_data_relationship["type"], dependency_type)
|
|
316
|
+
# else:
|
|
317
|
+
# log.debug(f"Adding new project entry for dependency: {dependency_id}")
|
|
318
|
+
# resolved_data[dependency_id] = {
|
|
319
|
+
# "metadata": {},
|
|
320
|
+
# "relationship": {
|
|
321
|
+
# "manual": False,
|
|
322
|
+
# "type": dependency_type,
|
|
323
|
+
# "dependencies": {},
|
|
324
|
+
# "dependants": {
|
|
325
|
+
# project_id: dependency_type
|
|
326
|
+
# }
|
|
327
|
+
# }
|
|
328
|
+
# }
|
|
329
|
+
|
|
330
|
+
# # Only process required mods
|
|
331
|
+
# if dependency_type == version_dependency_types["required"]:
|
|
332
|
+
# unresolved_ids.append(dependency_id)
|
|
333
|
+
|
|
334
|
+
# resolved_data[project_id] = project_data
|
|
335
|
+
|
|
336
|
+
# # Populate resolved_data with even more data
|
|
337
|
+
# fetch_ids = []
|
|
338
|
+
# for project_id, project_data in resolved_data.items():
|
|
339
|
+
# #If project data doesn't have slug, fetch it
|
|
340
|
+
# if not project_data["metadata"].get("project_slug"): fetch_ids.append(project_id)
|
|
341
|
+
|
|
342
|
+
# if fetch_ids != []:
|
|
343
|
+
# projects = json.loads(request(f"{modrinth_base_api}/v2/projects", query={
|
|
344
|
+
# "ids": json.dumps(fetch_ids)
|
|
345
|
+
# })["body"])
|
|
346
|
+
|
|
347
|
+
# for project in projects:
|
|
348
|
+
# if resolved_data[project["id"]]["relationship"]["type"] in (version_dependency_types["required"], version_dependency_types["incompatible"]):
|
|
349
|
+
# slug_id[project["slug"]] = {
|
|
350
|
+
# "id": project["id"]
|
|
351
|
+
# }
|
|
352
|
+
|
|
353
|
+
# project_metadata = resolved_data[project["id"]]["metadata"]
|
|
354
|
+
|
|
355
|
+
# project_metadata["project_slug"] = project["slug"]
|
|
356
|
+
# project_metadata["project_title"] = project["title"]
|
|
357
|
+
# project_metadata["project_description"] = project["description"]
|
|
358
|
+
# project_metadata["project_license"] = project["license"]
|
|
359
|
+
# project_metadata["loaders"] = project["loaders"]
|
|
360
|
+
|
|
361
|
+
# # Update with new slug to ID data
|
|
362
|
+
# slug_id_file.write_text(json.dumps(slug_id, indent=2))
|
|
363
|
+
|
|
364
|
+
# # Write project index
|
|
365
|
+
# for project_id, project_data in resolved_data.items():
|
|
366
|
+
# project_type = project_data["relationship"]["type"]
|
|
367
|
+
# if project_type in (version_dependency_types["required"], version_dependency_types["incompatible"]):
|
|
368
|
+
# log.debug(f"Indexing project '{project_id}' with type {project_type} and manual {project_data["relationship"]['manual']}")
|
|
369
|
+
# #write_project_index(project_id, project_data)
|
|
370
|
+
# if project_index_exists(project_id):
|
|
371
|
+
# update_project_index(project_id, project_data["metadata"], project_data["relationship"])
|
|
372
|
+
# else:
|
|
373
|
+
# create_project_index(project_id, project_data["metadata"], project_data["relationship"])
|
|
374
|
+
|
|
375
|
+
# if debug_mode: print(json.dumps(resolved_data, indent=2))
|
|
376
|
+
# return resolved_data
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# Command Functions
|
|
380
|
+
def add_projects(args):
|
|
381
|
+
# projects = args.projects
|
|
382
|
+
|
|
383
|
+
# if is_active():
|
|
384
|
+
# log.error(f"There's another MCServer processes running with process ID: {get_state()["process_id"]}")
|
|
385
|
+
# return 1
|
|
386
|
+
|
|
387
|
+
# server_config = load_config("server")
|
|
388
|
+
# server_loader = server_config["loader"]
|
|
389
|
+
|
|
390
|
+
# loader_name = server_loader["name"]
|
|
391
|
+
# server_version = server_config["version"]
|
|
392
|
+
|
|
393
|
+
# if server_loader["name"] == "vanilla":
|
|
394
|
+
# log.error("Vanilla Minecraft server does not support mod")
|
|
395
|
+
# return 1
|
|
396
|
+
|
|
397
|
+
# set_loader_context(loader_name)
|
|
398
|
+
|
|
399
|
+
# resolved_data = resolve_projects(projects, server_version, loader_name)
|
|
400
|
+
|
|
401
|
+
# # Get project IDs
|
|
402
|
+
# total_size = 0
|
|
403
|
+
|
|
404
|
+
# required_project_name = []
|
|
405
|
+
# optional_project_name = []
|
|
406
|
+
# incompatible_project_name = []
|
|
407
|
+
|
|
408
|
+
# for project in resolved_data.values():
|
|
409
|
+
# project_type = project["relationship"]["type"]
|
|
410
|
+
# if project_type == version_dependency_types["required"]:
|
|
411
|
+
# project_file = project["metadata"]["file"]
|
|
412
|
+
# if (loader_dir / project_file["filename"]).exists(): continue
|
|
413
|
+
|
|
414
|
+
# total_size += project_file["file_size"]
|
|
415
|
+
# required_project_name.append(f"{project["metadata"]['project_title']} ({project["metadata"]['project_slug']})")
|
|
416
|
+
# elif project_type == version_dependency_types["optional"]:
|
|
417
|
+
# optional_project_name.append(f"{project["metadata"]['project_title']} ({project["metadata"]['project_slug']})")
|
|
418
|
+
# elif project_type == version_dependency_types["incompatible"]:
|
|
419
|
+
# incompatible_project_name.append(f"{project["metadata"]['project_title']} ({project["metadata"]['project_slug']})")
|
|
420
|
+
|
|
421
|
+
# if required_project_name:
|
|
422
|
+
# required_project_count = len(required_project_name)
|
|
423
|
+
# log.info(f"{required_project_count} {pluralize(project_label, required_project_count)} that {pluralize('is', required_project_count)} going to be downloaded:")
|
|
424
|
+
# print(wrap_ansi(wrap_string(", ".join(required_project_name), initial_indent=" ", subsequent_indent=" "), "green"), end="\n\n")
|
|
425
|
+
|
|
426
|
+
# if optional_project_name:
|
|
427
|
+
# optional_project_count = len(optional_project_name)
|
|
428
|
+
# log.info(f"{optional_project_count} {pluralize(project_label, optional_project_count)} that {pluralize('is', optional_project_count)} optional:")
|
|
429
|
+
# print(wrap_ansi(wrap_string(", ".join(optional_project_name), initial_indent=" ", subsequent_indent=" "), "yellow"), end="\n\n")
|
|
430
|
+
|
|
431
|
+
# if incompatible_project_name:
|
|
432
|
+
# incompatible_project_count = len(incompatible_project_name)
|
|
433
|
+
# log.warning(f"{incompatible_project_count} {pluralize(project_label, incompatible_project_count)} that {pluralize('is', incompatible_project_count)} INCOMPATIBLE:")
|
|
434
|
+
# print(wrap_ansi(wrap_string(", ".join(incompatible_project_name), initial_indent=" ", subsequent_indent=" "), "red"), end="\n\n")
|
|
435
|
+
|
|
436
|
+
# if required_project_name:
|
|
437
|
+
# project_count = len(resolved_data)
|
|
438
|
+
# log.info(f"Will download {format_number(total_size, 'iec')} worth of {pluralize(project_label, project_count)} {pluralize("file", project_count)}.")
|
|
439
|
+
# else:
|
|
440
|
+
# log.info(f"All requested {pluralize(project_label, len(projects))} has been downloaded, nothing to do!")
|
|
441
|
+
# return 0
|
|
442
|
+
|
|
443
|
+
# answer = confirmation_prompt("Do you want to continue?", True)
|
|
444
|
+
# if not answer:
|
|
445
|
+
# print("Cancelled")
|
|
446
|
+
# return 1
|
|
447
|
+
|
|
448
|
+
# if not loader_dir:
|
|
449
|
+
# log.error("Cannot create directory for non-defined project type")
|
|
450
|
+
# return 1
|
|
451
|
+
|
|
452
|
+
# set_state("adding_project")
|
|
453
|
+
|
|
454
|
+
# loader_dir.mkdir(exist_ok=True)
|
|
455
|
+
# for project in resolved_data.values():
|
|
456
|
+
# if project["relationship"]["type"] == version_dependency_types["required"]:
|
|
457
|
+
# project_file = project["metadata"]["file"]
|
|
458
|
+
# if (loader_dir / project_file["filename"]).exists(): continue
|
|
459
|
+
|
|
460
|
+
# log.info(f"Downloading version {project["metadata"]['version_name']}...")
|
|
461
|
+
# download(project_file["url"], loader_dir / project_file["filename"], hashes=project_file["hashes"])
|
|
462
|
+
return 0
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def initialize_server(args):
|
|
466
|
+
# Arguments
|
|
467
|
+
game_version = args.mc_version
|
|
468
|
+
|
|
469
|
+
loader_name = args.loader
|
|
470
|
+
loader_version = args.loader_version
|
|
471
|
+
|
|
472
|
+
min_ram = args.min_ram
|
|
473
|
+
max_ram = args.max_ram
|
|
474
|
+
|
|
475
|
+
if loader_name == "vanilla":
|
|
476
|
+
loader_version = None
|
|
477
|
+
|
|
478
|
+
# Update project 'context'
|
|
479
|
+
set_loader_context(loader_name)
|
|
480
|
+
|
|
481
|
+
is_first_initialize = not metadata_file.exists()
|
|
482
|
+
|
|
483
|
+
# Special latest game_version
|
|
484
|
+
match game_version:
|
|
485
|
+
case "latest" | "latest-release":
|
|
486
|
+
game_version = mojang_manifest.get_latest_release_version()
|
|
487
|
+
log.info(f"Latest Minecraft release version is: {game_version}")
|
|
488
|
+
case "latest-snapshot":
|
|
489
|
+
game_version = mojang_manifest.get_latest_snapshot_version()
|
|
490
|
+
log.info(f"Latest Minecraft snapshot version is: {game_version}")
|
|
491
|
+
|
|
492
|
+
# Special latest loader_version
|
|
493
|
+
if loader_version == "latest":
|
|
494
|
+
match loader_name:
|
|
495
|
+
# Mod loaders
|
|
496
|
+
case "fabric":
|
|
497
|
+
latest_loader_version = ""
|
|
498
|
+
for version in fabricmc_meta.get_loader_versions():
|
|
499
|
+
if version["is_stable"]:
|
|
500
|
+
latest_loader_version = version["loader_version"]
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
log.info(f"Latest Fabric Loader version is {latest_loader_version}")
|
|
504
|
+
loader_version = latest_loader_version
|
|
505
|
+
|
|
506
|
+
# Plugin loaders
|
|
507
|
+
case "paper":
|
|
508
|
+
latest_build_version = papermc_api.get_latest_project_build(
|
|
509
|
+
"paper", game_version
|
|
510
|
+
)["build_version"]
|
|
511
|
+
log.info(f"Latest Paper build version is: {latest_build_version}")
|
|
512
|
+
loader_version = latest_build_version
|
|
513
|
+
case "purpur":
|
|
514
|
+
latest_build_version = purpurmc_api.get_latest_project_build(
|
|
515
|
+
"purpur", game_version
|
|
516
|
+
)["build_version"]
|
|
517
|
+
log.info(f"Latest Purpur build version is: {latest_build_version}")
|
|
518
|
+
loader_version = latest_build_version
|
|
519
|
+
|
|
520
|
+
# Creating the directories
|
|
521
|
+
mcserver_dir.mkdir(exist_ok=True)
|
|
522
|
+
configs_dir.mkdir(exist_ok=True)
|
|
523
|
+
# indexes_dir.mkdir(exist_ok=True)
|
|
524
|
+
|
|
525
|
+
tempfiles_dir.mkdir(exist_ok=True)
|
|
526
|
+
|
|
527
|
+
if loader_dir is not None:
|
|
528
|
+
loader_dir.mkdir(exist_ok=True)
|
|
529
|
+
|
|
530
|
+
# Creating the files
|
|
531
|
+
# if not slug_id_file.exists():
|
|
532
|
+
# slug_id_file.write_text(json.dumps({}, indent=2))
|
|
533
|
+
|
|
534
|
+
# Generate configuration files
|
|
535
|
+
for config_name in default_configs.keys():
|
|
536
|
+
update_config = None
|
|
537
|
+
match config_name:
|
|
538
|
+
case "launcher":
|
|
539
|
+
update_config = {"ram": {"min": min_ram, "max": max_ram}}
|
|
540
|
+
|
|
541
|
+
case "server":
|
|
542
|
+
update_config = {
|
|
543
|
+
"version": game_version,
|
|
544
|
+
"loader": {"name": loader_name, "version": loader_version},
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
generate_config(config_name, update_config)
|
|
549
|
+
except FileExistsError:
|
|
550
|
+
log.debug(
|
|
551
|
+
f"Configuration file '{config_name}' already exist, continuing..."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if is_first_initialize:
|
|
555
|
+
metadata_file.write_text(
|
|
556
|
+
json.dumps({"mcserver": {"version": __version__}}, indent=2)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
log.info("Initialized server configuration.")
|
|
560
|
+
else:
|
|
561
|
+
log.info("Reinitialized server configuration.")
|
|
562
|
+
|
|
563
|
+
return 0
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def import_setup(args):
|
|
567
|
+
file = Path(args.file)
|
|
568
|
+
|
|
569
|
+
min_ram = args.min_ram
|
|
570
|
+
max_ram = args.max_ram
|
|
571
|
+
|
|
572
|
+
modpack_index = modrinth_modpack.read_index(file)
|
|
573
|
+
|
|
574
|
+
loader_name = ""
|
|
575
|
+
loader_version = ""
|
|
576
|
+
for dependency_name, dependency_version in modpack_index["dependencies"].items():
|
|
577
|
+
match dependency_name:
|
|
578
|
+
case "minecraft":
|
|
579
|
+
continue
|
|
580
|
+
case "fabric-loader":
|
|
581
|
+
loader_name = "fabric"
|
|
582
|
+
loader_version = dependency_version
|
|
583
|
+
break
|
|
584
|
+
case _:
|
|
585
|
+
log.error(f"Loader '{dependency_name}' is not supported")
|
|
586
|
+
return 1
|
|
587
|
+
|
|
588
|
+
args = argparse.Namespace(
|
|
589
|
+
mc_version=modpack_index["dependencies"]["minecraft"],
|
|
590
|
+
loader=loader_name,
|
|
591
|
+
loader_version=loader_version,
|
|
592
|
+
min_ram=min_ram,
|
|
593
|
+
max_ram=max_ram,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
initialize_server(args)
|
|
597
|
+
|
|
598
|
+
# Downloading files
|
|
599
|
+
for file in modpack_index["files"]:
|
|
600
|
+
mod_environment = file.get("mod_environment")
|
|
601
|
+
|
|
602
|
+
if mod_environment is not None:
|
|
603
|
+
match mod_environment["server_side"]:
|
|
604
|
+
case "unsupported":
|
|
605
|
+
log.debug(f"Ignoring '{file['file_path']}', server is unsupported")
|
|
606
|
+
continue
|
|
607
|
+
case "optional":
|
|
608
|
+
pass
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def list_projects(args) -> int:
|
|
612
|
+
return 0
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
# 'op' commands
|
|
616
|
+
def op_grant(args) -> int:
|
|
617
|
+
players = args.players
|
|
618
|
+
permission_level = args.permission_level
|
|
619
|
+
bypasses_player_limit = args.bypasses_player_limit
|
|
620
|
+
return 0
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def op_list(args) -> int:
|
|
624
|
+
return 0
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def op_revoke(args) -> int:
|
|
628
|
+
players = args.players
|
|
629
|
+
return 0
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def search_projects(args):
|
|
633
|
+
query = " ".join(args.query)
|
|
634
|
+
no_filter = args.no_filter
|
|
635
|
+
|
|
636
|
+
page = args.page
|
|
637
|
+
sort_by = args.sort_by
|
|
638
|
+
|
|
639
|
+
modrinth_config = load_config("modrinth", allow_missing=True)
|
|
640
|
+
search_limit = modrinth_config["search_limit"]
|
|
641
|
+
|
|
642
|
+
search_index = sort_by or modrinth_config.get("sort_by", "relevance")
|
|
643
|
+
search_offset = search_limit * (page - 1)
|
|
644
|
+
search_message = (
|
|
645
|
+
f"Searching projects with '{query}' in Modrinth sorted by {search_index}..."
|
|
646
|
+
)
|
|
647
|
+
search_filters = []
|
|
648
|
+
|
|
649
|
+
if not (configs_dir / "server.json").exists():
|
|
650
|
+
log.warning(
|
|
651
|
+
"Configuration file for 'server' is missing, no version filter will be applied."
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
search_filters = []
|
|
655
|
+
search_message = (
|
|
656
|
+
f"Searching projects with '{query}' in Modrinth sorted by {search_index}..."
|
|
657
|
+
)
|
|
658
|
+
elif not no_filter:
|
|
659
|
+
server_config = load_config("server")
|
|
660
|
+
server_loader = server_config["loader"]
|
|
661
|
+
|
|
662
|
+
server_version = server_config["version"]
|
|
663
|
+
loader_name = server_loader["name"]
|
|
664
|
+
|
|
665
|
+
set_loader_context(loader_name)
|
|
666
|
+
|
|
667
|
+
if loader_name == "vanilla":
|
|
668
|
+
raise SearchProjectsError("Vanilla Minecraft server does not support mod.")
|
|
669
|
+
|
|
670
|
+
search_message = f"Searching {pluralize(project_label)} with '{query}' in Modrinth for {loader_name.capitalize()}, Minecraft server {server_version} sorted by {search_index}..."
|
|
671
|
+
search_filters = [
|
|
672
|
+
[f"project_type:{project_label}"],
|
|
673
|
+
["server_side!=unsupported"],
|
|
674
|
+
[f"categories:{loader_name}"],
|
|
675
|
+
[f"versions:{server_version}"],
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
log.info(search_message)
|
|
679
|
+
response = json.loads(
|
|
680
|
+
request(
|
|
681
|
+
f"{modrinth_base_api}/v2/search",
|
|
682
|
+
query={
|
|
683
|
+
"query": query,
|
|
684
|
+
"facets": json.dumps(search_filters),
|
|
685
|
+
"index": search_index,
|
|
686
|
+
"limit": search_limit,
|
|
687
|
+
"offset": search_offset,
|
|
688
|
+
},
|
|
689
|
+
)["body"]
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
projects_list = response.get("hits", [])
|
|
693
|
+
if not projects_list:
|
|
694
|
+
log.error(f"No {project_label} found.")
|
|
695
|
+
return 1
|
|
696
|
+
|
|
697
|
+
server_side_width = 0
|
|
698
|
+
client_side_width = 0
|
|
699
|
+
|
|
700
|
+
downloads_width = 0
|
|
701
|
+
follows_width = 0
|
|
702
|
+
|
|
703
|
+
for project in projects_list:
|
|
704
|
+
server_side_width = max(
|
|
705
|
+
server_side_width, len(project.get("server_side", "unknown"))
|
|
706
|
+
)
|
|
707
|
+
client_side_width = max(
|
|
708
|
+
client_side_width, len(project.get("client_side", "unknown"))
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
downloads = project.get("downloads", 0)
|
|
712
|
+
downloads_width = max(
|
|
713
|
+
downloads_width,
|
|
714
|
+
len(f"{pluralize('Download', downloads)}: {format_number(downloads)}"),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
follows = project.get("follows", 0)
|
|
718
|
+
follows_width = max(
|
|
719
|
+
follows_width,
|
|
720
|
+
len(f"{pluralize('Follow', follows)}: {format_number(follows)}"),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
total_hits = response.get("total_hits", 0)
|
|
724
|
+
total_pages = f"{math.ceil(total_hits / search_limit):,}"
|
|
725
|
+
|
|
726
|
+
response_offset = response.get("offset", 0)
|
|
727
|
+
shown_projects_count = min(len(projects_list), search_limit)
|
|
728
|
+
|
|
729
|
+
skipped_message = (
|
|
730
|
+
f"(skipped {response_offset:,} {pluralize(project_label, response_offset)}) "
|
|
731
|
+
if response_offset > 0
|
|
732
|
+
else ""
|
|
733
|
+
)
|
|
734
|
+
log.info(
|
|
735
|
+
f"Showing {shown_projects_count:,} {pluralize(project_label, shown_projects_count)} out of {total_hits:,} total {pluralize(project_label, total_hits)} {skipped_message}| Page: {page:,}/{total_pages}"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
for index, project in enumerate(projects_list):
|
|
739
|
+
title = wrap_ansi(project.get("title", "<No Title>"), "bold")
|
|
740
|
+
slug = project.get("slug", "<No Slug>")
|
|
741
|
+
author = f"by {project.get('author', '<No Author>')}"
|
|
742
|
+
|
|
743
|
+
project_header = wrap_string(
|
|
744
|
+
f"{title} ({slug}) {wrap_ansi(author, 'gray')}", subsequent_indent=" "
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
downloads = project.get("downloads", 0)
|
|
748
|
+
follows = project.get("follows", 0)
|
|
749
|
+
|
|
750
|
+
downloads_text = (
|
|
751
|
+
f"{pluralize('Download', downloads)}: {format_number(downloads)}"
|
|
752
|
+
)
|
|
753
|
+
follows_text = f"{pluralize('Follow', follows)}: {format_number(follows)}"
|
|
754
|
+
downloads_follows = wrap_ansi(
|
|
755
|
+
f"[ {downloads_text: <{downloads_width}} | {follows_text: <{follows_width}} ]",
|
|
756
|
+
"gray",
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
server_side = wrap_ansi(
|
|
760
|
+
f"Server: {mod_environment_color(project.get('server_side', "unknown"), server_side_width)}",
|
|
761
|
+
"bold",
|
|
762
|
+
)
|
|
763
|
+
client_side = mod_environment_color(
|
|
764
|
+
project.get("client_side", "unknown"), client_side_width
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
description = wrap_string(
|
|
768
|
+
project["description"], initial_indent=" ", subsequent_indent=" "
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
is_end_of_list = (index + 1) == shown_projects_count
|
|
772
|
+
|
|
773
|
+
print(
|
|
774
|
+
f"""{project_header}
|
|
775
|
+
[ {server_side} | Client: {client_side} ] {downloads_follows}
|
|
776
|
+
> https://modrinth.com/{project_label}/{slug}
|
|
777
|
+
{description}""",
|
|
778
|
+
end="\n" if is_end_of_list else "\n\n",
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def show_projects(args):
|
|
783
|
+
projects = args.projects
|
|
784
|
+
|
|
785
|
+
set_loader_context(load_config("server", allow_missing=True)["loader"]["name"])
|
|
786
|
+
|
|
787
|
+
log.info(f"Getting {pluralize(project_label, len(projects))} information...")
|
|
788
|
+
projects_info = json.loads(
|
|
789
|
+
request(
|
|
790
|
+
f"{modrinth_base_api}/v2/projects", query={"ids": json.dumps(projects)}
|
|
791
|
+
)["body"]
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
for project_index, project in enumerate(projects_info):
|
|
795
|
+
title = wrap_ansi(project.get("title", "") or "<No Title>", "bold")
|
|
796
|
+
slug = wrap_ansi(project.get("slug", f"<{projects[project_index]}>"), "bold")
|
|
797
|
+
|
|
798
|
+
downloads = wrap_ansi(format_number(project.get("downloads", 0)), "bold")
|
|
799
|
+
followers = wrap_ansi(format_number(project.get("followers", 0)), "bold")
|
|
800
|
+
|
|
801
|
+
project_categories = ", ".join(project.get("categories", ["None"])).title()
|
|
802
|
+
categories = wrap_string(
|
|
803
|
+
f"Categories: {wrap_ansi(project_categories, 'bold')}",
|
|
804
|
+
subsequent_indent=" ",
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
project_loaders = ", ".join(project.get("loaders", ["None"])).title()
|
|
808
|
+
loaders = wrap_string(
|
|
809
|
+
f"Loaders: {wrap_ansi(project_loaders, 'bold')}", subsequent_indent=" "
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
project_game_versions = ", ".join(
|
|
813
|
+
project.get("game_versions", ["None"])
|
|
814
|
+
).title()
|
|
815
|
+
game_versions = wrap_string(
|
|
816
|
+
f"Minecraft Versions: {wrap_ansi(project_game_versions, 'bold')}",
|
|
817
|
+
subsequent_indent=" ",
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
server_side = wrap_ansi(
|
|
821
|
+
f"Server: {mod_environment_color(project.get('server_side', '<Unknown>'))}",
|
|
822
|
+
"bold",
|
|
823
|
+
)
|
|
824
|
+
client_side = mod_environment_color(project.get("client_side", "<Unknown>"))
|
|
825
|
+
|
|
826
|
+
project_license = project.get("license", {})
|
|
827
|
+
license_name = wrap_ansi(
|
|
828
|
+
project_license.get("name", "") or "All Rights Reserved", "bold"
|
|
829
|
+
)
|
|
830
|
+
license_id = wrap_ansi(
|
|
831
|
+
project_license.get("id", "") or "LicenseRef-All-Rights-Reserved", "bold"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
project_homepage = project.get("slug", project["id"])
|
|
835
|
+
homepage = wrap_ansi(
|
|
836
|
+
f"https://modrinth.com/{project_label}/{project_homepage}", "bold"
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
project_description = project.get("description", "<No Description>")
|
|
840
|
+
description = wrap_string(
|
|
841
|
+
f"Description: {wrap_ansi(project_description, 'bold')}",
|
|
842
|
+
subsequent_indent=" ",
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
if project_index >= 1:
|
|
846
|
+
print()
|
|
847
|
+
print(f"""Name: {title} ({slug})
|
|
848
|
+
Downloads: {downloads}
|
|
849
|
+
Followers: {followers}
|
|
850
|
+
{categories}
|
|
851
|
+
{loaders}
|
|
852
|
+
{game_versions}
|
|
853
|
+
Environment: {server_side} | Client: {client_side}
|
|
854
|
+
License: {license_name} ({license_id})
|
|
855
|
+
Homepage: {homepage}
|
|
856
|
+
{description}""")
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def start_server(args):
|
|
860
|
+
# Checking current state
|
|
861
|
+
if state.is_active():
|
|
862
|
+
current_state = state.get_state()
|
|
863
|
+
log.error(
|
|
864
|
+
f"There's another active MCServer process ({current_state["action"]}) running with process ID: {current_state["process_id"]}"
|
|
865
|
+
)
|
|
866
|
+
return 1
|
|
867
|
+
|
|
868
|
+
state.set_state("running_server")
|
|
869
|
+
|
|
870
|
+
# Loading configs
|
|
871
|
+
server_config = config.load_config("server")
|
|
872
|
+
launcher_config = config.load_config("launcher")
|
|
873
|
+
|
|
874
|
+
server_loader = server_config["loader"]
|
|
875
|
+
game_version = server_config["version"]
|
|
876
|
+
|
|
877
|
+
loader_name = server_loader["name"]
|
|
878
|
+
loader_version = server_loader["version"]
|
|
879
|
+
|
|
880
|
+
jarfile = launcher_config["jarfile"]
|
|
881
|
+
memory_config = launcher_config["ram"]
|
|
882
|
+
|
|
883
|
+
# Download jarfile
|
|
884
|
+
if not Path(jarfile).exists():
|
|
885
|
+
match loader_name:
|
|
886
|
+
# Mod loaders
|
|
887
|
+
case "fabric":
|
|
888
|
+
log.info(
|
|
889
|
+
f"Downloading Fabric loader {loader_version} for Minecraft version {game_version}..."
|
|
890
|
+
)
|
|
891
|
+
networking.download(
|
|
892
|
+
f"https://meta.fabricmc.net/v2/versions/loader/{game_version}/{loader_version}/1.1.1/server/jar",
|
|
893
|
+
launcher_config["jarfile"],
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
# Plugin loaders
|
|
897
|
+
case "paper":
|
|
898
|
+
log.info(
|
|
899
|
+
f"Downloading Paper version {loader_version} for Minecraft version {game_version}..."
|
|
900
|
+
)
|
|
901
|
+
download_prop = papermc_api.get_project_build(
|
|
902
|
+
"paper", game_version, loader_version
|
|
903
|
+
)["download_props"]["server:default"]
|
|
904
|
+
networking.download(
|
|
905
|
+
download_prop["download_url"],
|
|
906
|
+
launcher_config["jarfile"],
|
|
907
|
+
dict(download_prop["checksums"]),
|
|
908
|
+
)
|
|
909
|
+
case "purpur":
|
|
910
|
+
log.info(
|
|
911
|
+
f"Downloading Purpur version {loader_version} for Minecraft version {game_version}..."
|
|
912
|
+
)
|
|
913
|
+
networking.download(
|
|
914
|
+
purpurmc_api.download_url("purpur", game_version, loader_version),
|
|
915
|
+
launcher_config["jarfile"],
|
|
916
|
+
{
|
|
917
|
+
"md5": purpurmc_api.get_project_build(
|
|
918
|
+
"purpur", game_version, loader_version
|
|
919
|
+
)["artifact_md5"]
|
|
920
|
+
},
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
# Vanilla
|
|
924
|
+
case "vanilla":
|
|
925
|
+
log.debug("Getting Mojang version manifest file...")
|
|
926
|
+
version_manifest = mojang_manifest.get_version_manifest()[
|
|
927
|
+
"game_versions"
|
|
928
|
+
]
|
|
929
|
+
|
|
930
|
+
selected_version_url = None
|
|
931
|
+
for version in version_manifest:
|
|
932
|
+
if version["version_id"] == game_version:
|
|
933
|
+
selected_version_url = version["package_url"]
|
|
934
|
+
break
|
|
935
|
+
|
|
936
|
+
if selected_version_url is None:
|
|
937
|
+
log.error(f"Minecraft version {game_version} is not found.")
|
|
938
|
+
return 1
|
|
939
|
+
|
|
940
|
+
server_download = json.loads(
|
|
941
|
+
networking.request(selected_version_url)["body"]
|
|
942
|
+
)["downloads"]["server"]
|
|
943
|
+
log.info(f"Downloading vanilla Minecraft version {game_version}...")
|
|
944
|
+
|
|
945
|
+
networking.download(
|
|
946
|
+
server_download["url"],
|
|
947
|
+
launcher_config["jarfile"],
|
|
948
|
+
{"sha1": server_download["sha1"]},
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
case _:
|
|
952
|
+
log.error(f"Loader '{loader_name}' is not supported.")
|
|
953
|
+
return 1
|
|
954
|
+
|
|
955
|
+
# Running the server
|
|
956
|
+
if not shutil.which("java"):
|
|
957
|
+
log.error(
|
|
958
|
+
"Cannot find 'java' in PATH, is Java Runtime Environment installed correctly?"
|
|
959
|
+
)
|
|
960
|
+
return 1
|
|
961
|
+
|
|
962
|
+
if memory_config["min"] > memory_config["max"]:
|
|
963
|
+
log.error(
|
|
964
|
+
f"Minimum RAM cannot be larger that maximum RAM, please check configuration for `launcher`"
|
|
965
|
+
)
|
|
966
|
+
return 1
|
|
967
|
+
|
|
968
|
+
# TODO: Implement the "free RAM" checker here
|
|
969
|
+
|
|
970
|
+
try:
|
|
971
|
+
if not mojang_eula.is_eula_agreed():
|
|
972
|
+
log.warning(
|
|
973
|
+
"You need to agree to Mojang's EULA in order to run the server: https://aka.ms/MinecraftEULA"
|
|
974
|
+
)
|
|
975
|
+
log.info(
|
|
976
|
+
f"Please type '{wrap_ansi(eula_agree_sentence, 'yellow')}' (case-insensitive) to agree with Mojang's EULA."
|
|
977
|
+
)
|
|
978
|
+
answer = input("> ")
|
|
979
|
+
|
|
980
|
+
if answer.lower().strip() == eula_agree_sentence.lower().strip():
|
|
981
|
+
mojang_eula.eula_agree()
|
|
982
|
+
else:
|
|
983
|
+
log.error("Failed to start the Minecraft server: EULA not agreed")
|
|
984
|
+
return 1
|
|
985
|
+
except KeyboardInterrupt:
|
|
986
|
+
log.error("\nFailed to start the Minecraft server: EULA not agreed")
|
|
987
|
+
return 1
|
|
988
|
+
|
|
989
|
+
java_command_argv = [
|
|
990
|
+
"java",
|
|
991
|
+
f"-Xmx{memory_config['max']}M",
|
|
992
|
+
f"-Xms{memory_config['min']}M",
|
|
993
|
+
"-jar",
|
|
994
|
+
launcher_config["jarfile"],
|
|
995
|
+
]
|
|
996
|
+
|
|
997
|
+
if launcher_config["hide_gui"]:
|
|
998
|
+
java_command_argv.append("nogui")
|
|
999
|
+
|
|
1000
|
+
log.info("Starting Minecraft server...")
|
|
1001
|
+
|
|
1002
|
+
log.debug(f"Executing command: {' '.join(java_command_argv)}")
|
|
1003
|
+
os.execvp(java_command_argv[0], java_command_argv)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def whitelist_add(args) -> int:
|
|
1007
|
+
players = args.players
|
|
1008
|
+
return 0
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def whitelist_list(args) -> int:
|
|
1012
|
+
whitelisted_players = server_whitelist.list_players()
|
|
1013
|
+
player_count = len(whitelisted_players)
|
|
1014
|
+
if player_count < 1:
|
|
1015
|
+
log.info("There is no whitelisted player")
|
|
1016
|
+
else:
|
|
1017
|
+
log.info(
|
|
1018
|
+
f"There {pluralize('is', player_count)} {player_count} whitelisted {pluralize("player", player_count)}:"
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
player_list: list[str] = []
|
|
1022
|
+
for player in whitelisted_players:
|
|
1023
|
+
player_list.append(f"{player['player_name']} ({player['player_uuid']})")
|
|
1024
|
+
|
|
1025
|
+
player_list.sort()
|
|
1026
|
+
print("\n".join(player_list))
|
|
1027
|
+
return 0
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def whitelist_remove(args) -> int:
|
|
1031
|
+
players = args.players
|
|
1032
|
+
return 0
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# Log handler
|
|
1036
|
+
class ClearLineHandler(log.StreamHandler):
|
|
1037
|
+
def emit(self, record):
|
|
1038
|
+
self.stream.write(ansi("clear_line") + ansi("start_line"))
|
|
1039
|
+
self.stream.flush()
|
|
1040
|
+
super().emit(record)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
# Main
|
|
1044
|
+
def main():
|
|
1045
|
+
if (sys.argv[1] if len(sys.argv) > 1 else "") == "cake":
|
|
1046
|
+
hahaha_yes()
|
|
1047
|
+
|
|
1048
|
+
log_handler = ClearLineHandler(sys.stdout)
|
|
1049
|
+
log_handler.setFormatter(log.Formatter("[%(levelname)s]: %(message)s"))
|
|
1050
|
+
|
|
1051
|
+
log.basicConfig(level=log_level, handlers=[log_handler])
|
|
1052
|
+
|
|
1053
|
+
# Parser
|
|
1054
|
+
parser = argparse.ArgumentParser(
|
|
1055
|
+
prog="mcserver",
|
|
1056
|
+
description="A CLI tool for managing Minecraft: Java Edition servers.",
|
|
1057
|
+
epilog="Not created for Windows.",
|
|
1058
|
+
allow_abbrev=False,
|
|
1059
|
+
suggest_on_error=True,
|
|
1060
|
+
color=True,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
parser.set_defaults(func=print_help, parser=parser)
|
|
1064
|
+
|
|
1065
|
+
parser.add_argument(
|
|
1066
|
+
"-v", "--version", action="version", version="%(prog)s " + __version__
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
commands = parser.add_subparsers(title="Commands")
|
|
1070
|
+
|
|
1071
|
+
# Commands
|
|
1072
|
+
# 'add' command
|
|
1073
|
+
add_command = commands.add_parser(
|
|
1074
|
+
"add", help="Add Modrinth projects", description="Add Modrinth projects"
|
|
1075
|
+
)
|
|
1076
|
+
add_command.set_defaults(func=add_projects)
|
|
1077
|
+
|
|
1078
|
+
add_command.add_argument(
|
|
1079
|
+
"projects", nargs="+", type=str, help="Project slugs or IDs"
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
# 'import' command
|
|
1083
|
+
import_command = commands.add_parser(
|
|
1084
|
+
"import",
|
|
1085
|
+
description="Import a Modrinth modpack",
|
|
1086
|
+
help="Import a Modrinth modpack",
|
|
1087
|
+
)
|
|
1088
|
+
import_command.set_defaults(func=import_setup)
|
|
1089
|
+
|
|
1090
|
+
import_command.add_argument("file", type=str, help="THe Modpack file")
|
|
1091
|
+
import_command.add_argument(
|
|
1092
|
+
"--min-ram",
|
|
1093
|
+
default=512,
|
|
1094
|
+
type=int,
|
|
1095
|
+
help="Minimum RAM for the server (in Mebibytes)",
|
|
1096
|
+
metavar="size",
|
|
1097
|
+
)
|
|
1098
|
+
import_command.add_argument(
|
|
1099
|
+
"--max-ram",
|
|
1100
|
+
default=2048,
|
|
1101
|
+
type=int,
|
|
1102
|
+
help="Maximum RAM for the server (in Mebibytes)",
|
|
1103
|
+
metavar="size",
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
# 'init' command
|
|
1107
|
+
init_command = commands.add_parser("init", help="Initialize server configurations")
|
|
1108
|
+
init_command.set_defaults(func=initialize_server)
|
|
1109
|
+
|
|
1110
|
+
init_command.add_argument(
|
|
1111
|
+
"--mc-version",
|
|
1112
|
+
default="latest-release",
|
|
1113
|
+
type=str,
|
|
1114
|
+
help="Minecraft server version",
|
|
1115
|
+
metavar="version",
|
|
1116
|
+
)
|
|
1117
|
+
init_command.add_argument(
|
|
1118
|
+
"--loader",
|
|
1119
|
+
default="vanilla",
|
|
1120
|
+
type=str,
|
|
1121
|
+
choices=loaders_list,
|
|
1122
|
+
help="Loader for the server (vanilla for unmodded server)",
|
|
1123
|
+
)
|
|
1124
|
+
init_command.add_argument(
|
|
1125
|
+
"--loader-version",
|
|
1126
|
+
default="latest",
|
|
1127
|
+
type=str,
|
|
1128
|
+
help="Version of the loader",
|
|
1129
|
+
metavar="version",
|
|
1130
|
+
)
|
|
1131
|
+
init_command.add_argument(
|
|
1132
|
+
"--min-ram",
|
|
1133
|
+
default=512,
|
|
1134
|
+
type=int,
|
|
1135
|
+
help="Minimum RAM for the server (in Mebibyte)",
|
|
1136
|
+
metavar="size",
|
|
1137
|
+
)
|
|
1138
|
+
init_command.add_argument(
|
|
1139
|
+
"--max-ram",
|
|
1140
|
+
default=2048,
|
|
1141
|
+
type=int,
|
|
1142
|
+
help="Maximum RAM for the server (in Mebibyte)",
|
|
1143
|
+
metavar="size",
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
# 'list' command
|
|
1147
|
+
list_command = commands.add_parser(
|
|
1148
|
+
"list", description="List downloaded projects", help="List downloaded projects"
|
|
1149
|
+
)
|
|
1150
|
+
list_command.set_defaults(func=list_projects)
|
|
1151
|
+
|
|
1152
|
+
# 'op' commands
|
|
1153
|
+
op_command = commands.add_parser(
|
|
1154
|
+
"op", help="Manage operator statuses", description="Manage operator statuses"
|
|
1155
|
+
)
|
|
1156
|
+
op_command.set_defaults(func=print_help, parser=op_command)
|
|
1157
|
+
|
|
1158
|
+
op_subcommands = op_command.add_subparsers(title="Subcommands")
|
|
1159
|
+
|
|
1160
|
+
# 'op grant'
|
|
1161
|
+
op_grant_command = op_subcommands.add_parser(
|
|
1162
|
+
"grant",
|
|
1163
|
+
help="Grant operator status to players",
|
|
1164
|
+
description="Grant operator status to players",
|
|
1165
|
+
)
|
|
1166
|
+
op_grant_command.set_defaults(func=op_grant)
|
|
1167
|
+
|
|
1168
|
+
op_grant_command.add_argument("players", nargs="+", help="Player names or UUIDs")
|
|
1169
|
+
op_grant_command.add_argument(
|
|
1170
|
+
"--level",
|
|
1171
|
+
type=int,
|
|
1172
|
+
choices=[0, 1, 2, 3, 4],
|
|
1173
|
+
help="Operator permission level (0-4)",
|
|
1174
|
+
metavar="level",
|
|
1175
|
+
dest="permission_level",
|
|
1176
|
+
)
|
|
1177
|
+
op_grant_command.add_argument(
|
|
1178
|
+
"--bypass-player-limit",
|
|
1179
|
+
action="store_true",
|
|
1180
|
+
help="Can bypass player limit",
|
|
1181
|
+
dest="bypasses_player_limit",
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
# 'op list'
|
|
1185
|
+
op_list_command = op_subcommands.add_parser(
|
|
1186
|
+
"list", help="List all operators", description="List all operators"
|
|
1187
|
+
)
|
|
1188
|
+
op_list_command.set_defaults(func=op_list)
|
|
1189
|
+
|
|
1190
|
+
# 'op revoke'
|
|
1191
|
+
op_revoke_command = op_subcommands.add_parser(
|
|
1192
|
+
"revoke",
|
|
1193
|
+
help="Revoke operator status from players",
|
|
1194
|
+
description="Revoke operator status from players",
|
|
1195
|
+
)
|
|
1196
|
+
op_revoke_command.set_defaults(func=op_revoke)
|
|
1197
|
+
|
|
1198
|
+
op_revoke_command.add_argument("players", nargs="+", help="Player names or UUIDs")
|
|
1199
|
+
|
|
1200
|
+
# 'search' command
|
|
1201
|
+
search_command = commands.add_parser(
|
|
1202
|
+
"search",
|
|
1203
|
+
help="Search Modrinth projects",
|
|
1204
|
+
description="Search Modrinth projects",
|
|
1205
|
+
)
|
|
1206
|
+
search_command.set_defaults(func=search_projects)
|
|
1207
|
+
|
|
1208
|
+
search_command.add_argument("query", nargs="*", type=str, help="Search query")
|
|
1209
|
+
search_command.add_argument(
|
|
1210
|
+
"-n",
|
|
1211
|
+
"--no-filter",
|
|
1212
|
+
action="store_true",
|
|
1213
|
+
help="No search filter from server configuration",
|
|
1214
|
+
)
|
|
1215
|
+
search_command.add_argument(
|
|
1216
|
+
"-p", "--page", default=1, type=int, help="Page number", metavar="N"
|
|
1217
|
+
)
|
|
1218
|
+
search_command.add_argument(
|
|
1219
|
+
"-s",
|
|
1220
|
+
"--sort-by",
|
|
1221
|
+
default="relevance",
|
|
1222
|
+
type=str,
|
|
1223
|
+
choices=["downloads", "follows", "newest", "relevance", "updated"],
|
|
1224
|
+
help="Sort search result by",
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
# 'show' command
|
|
1228
|
+
show_command = commands.add_parser(
|
|
1229
|
+
"show", help="Show project information", description="Show project information"
|
|
1230
|
+
)
|
|
1231
|
+
show_command.set_defaults(func=show_projects)
|
|
1232
|
+
|
|
1233
|
+
show_command.add_argument(
|
|
1234
|
+
"projects", nargs="+", type=str, help="Project slugs or IDs"
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
# 'start' command
|
|
1238
|
+
start_command = commands.add_parser(
|
|
1239
|
+
"start", description="Start the server", help="Start the server"
|
|
1240
|
+
)
|
|
1241
|
+
start_command.set_defaults(func=start_server)
|
|
1242
|
+
|
|
1243
|
+
# 'whitelist' command
|
|
1244
|
+
whitelist_command = commands.add_parser(
|
|
1245
|
+
"whitelist",
|
|
1246
|
+
help="Manage whitelisted players",
|
|
1247
|
+
description="Manage whitelisted players",
|
|
1248
|
+
)
|
|
1249
|
+
whitelist_command.set_defaults(func=print_help, parser=whitelist_command)
|
|
1250
|
+
|
|
1251
|
+
whitelist_subcommands = whitelist_command.add_subparsers(title="Subcommands")
|
|
1252
|
+
|
|
1253
|
+
# 'whitelist add'
|
|
1254
|
+
whitelist_add_command = whitelist_subcommands.add_parser(
|
|
1255
|
+
"add",
|
|
1256
|
+
help="Add players to the whitelist",
|
|
1257
|
+
description="Add players to the whitelist",
|
|
1258
|
+
)
|
|
1259
|
+
whitelist_add_command.set_defaults(func=whitelist_add)
|
|
1260
|
+
|
|
1261
|
+
whitelist_add_command.add_argument(
|
|
1262
|
+
"players", nargs="+", help="Player names or UUIDs"
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
# 'whitelist list'
|
|
1266
|
+
whitelist_list_command = whitelist_subcommands.add_parser(
|
|
1267
|
+
"list",
|
|
1268
|
+
help="List all whitelisted players",
|
|
1269
|
+
description="List all whitelisted players",
|
|
1270
|
+
)
|
|
1271
|
+
whitelist_list_command.set_defaults(func=whitelist_list)
|
|
1272
|
+
|
|
1273
|
+
# 'whitelist remove'
|
|
1274
|
+
whitelist_remove_command = whitelist_subcommands.add_parser(
|
|
1275
|
+
"remove",
|
|
1276
|
+
help="Remove players from the whitelist",
|
|
1277
|
+
description="Remove players from the whitelist",
|
|
1278
|
+
)
|
|
1279
|
+
whitelist_remove_command.set_defaults(func=whitelist_remove)
|
|
1280
|
+
|
|
1281
|
+
whitelist_remove_command.add_argument(
|
|
1282
|
+
"players", nargs="+", help="Player names or UUIDs"
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
args = parser.parse_args()
|
|
1286
|
+
sys.exit(args.func(args))
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
### This file used to be a one big script file, like 1,200 lines of codes.
|
|
1290
|
+
### and the modularization happens after I found out about zipapp, silly me.
|
|
1291
|
+
### The reason that I might "reinvent the wheel" is because I used to want it just
|
|
1292
|
+
### download script and run without the 'pip install -r requirements.txt' ritual
|
|
1293
|
+
### and that is way before I knew about zipapp, so I can't bundle requests or
|
|
1294
|
+
### something. blBut now? I technically can, but I'll just continue the
|
|
1295
|
+
### dependency-free code, for fun and learning :shrug:. that means 100% of the
|
|
1296
|
+
### projectis written by me. yep, absolutely every bit of this prpject, no
|
|
1297
|
+
### external code at all!
|