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.
Files changed (61) hide show
  1. mcserver/__init__.py +1 -0
  2. mcserver/__main__.py +3 -0
  3. mcserver/app.py +24 -0
  4. mcserver/caching.py +0 -0
  5. mcserver/cli.py +326 -0
  6. mcserver/commands/__init__.py +39 -0
  7. mcserver/commands/add_projects.py +7 -0
  8. mcserver/commands/import_setup.py +6 -0
  9. mcserver/commands/initialize_server.py +119 -0
  10. mcserver/commands/list_projects.py +6 -0
  11. mcserver/commands/op_grant.py +7 -0
  12. mcserver/commands/op_list.py +32 -0
  13. mcserver/commands/op_revoke.py +7 -0
  14. mcserver/commands/print_help.py +7 -0
  15. mcserver/commands/remove_projects.py +6 -0
  16. mcserver/commands/search_projects.py +7 -0
  17. mcserver/commands/server_status.py +6 -0
  18. mcserver/commands/show_projects.py +26 -0
  19. mcserver/commands/start_server.py +150 -0
  20. mcserver/commands/stop_server.py +46 -0
  21. mcserver/commands/update_projects.py +6 -0
  22. mcserver/commands/whitelist_add.py +7 -0
  23. mcserver/commands/whitelist_list.py +31 -0
  24. mcserver/commands/whitelist_remove.py +31 -0
  25. mcserver/commandsa.py +1297 -0
  26. mcserver/config.py +37 -0
  27. mcserver/constants.py +14 -0
  28. mcserver/fabricmc/meta.py +41 -0
  29. mcserver/legacyfabric/meta.py +37 -0
  30. mcserver/metadata.py +33 -0
  31. mcserver/minecraft/api.py +94 -0
  32. mcserver/minecraft/player_identity.py +21 -0
  33. mcserver/modrinth/api.py +657 -0
  34. mcserver/modrinth/modpack.py +101 -0
  35. mcserver/modrinth/project_records/metadata.py +0 -0
  36. mcserver/modrinth/project_records/relationships.py +203 -0
  37. mcserver/modrinth/project_records/shared.py +9 -0
  38. mcserver/modrinth/projects.py +80 -0
  39. mcserver/modrinth/shared.py +13 -0
  40. mcserver/modrinth/slug_index.py +36 -0
  41. mcserver/mojang/eula.py +28 -0
  42. mcserver/mojang/manifest.py +69 -0
  43. mcserver/mysterious.py +5 -0
  44. mcserver/networking.py +152 -0
  45. mcserver/papermc/api.py +81 -0
  46. mcserver/purpurmc/api.py +81 -0
  47. mcserver/quiltmc/meta.py +54 -0
  48. mcserver/resolver.py +164 -0
  49. mcserver/server_files/banned_ips.py +120 -0
  50. mcserver/server_files/banned_players.py +166 -0
  51. mcserver/server_files/ops.py +143 -0
  52. mcserver/server_files/properties.py +41 -0
  53. mcserver/server_files/usercaches.py +0 -0
  54. mcserver/server_files/whitelist.py +163 -0
  55. mcserver/shared.py +198 -0
  56. mcserver/state.py +77 -0
  57. mcserver_cli-3.0.0.dist-info/METADATA +63 -0
  58. mcserver_cli-3.0.0.dist-info/RECORD +61 -0
  59. mcserver_cli-3.0.0.dist-info/WHEEL +4 -0
  60. mcserver_cli-3.0.0.dist-info/entry_points.txt +2 -0
  61. 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!