mc-creative-clone 0.1.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.
- mc_creative_clone/__init__.py +3 -0
- mc_creative_clone/main.py +412 -0
- mc_creative_clone-0.1.0.dist-info/METADATA +280 -0
- mc_creative_clone-0.1.0.dist-info/RECORD +7 -0
- mc_creative_clone-0.1.0.dist-info/WHEEL +4 -0
- mc_creative_clone-0.1.0.dist-info/entry_points.txt +2 -0
- mc_creative_clone-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from datetime import date
|
|
3
|
+
import logging
|
|
4
|
+
from os import scandir
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
from shutil import copytree, move, rmtree, which
|
|
8
|
+
from subprocess import CalledProcessError, run
|
|
9
|
+
from sys import exit, platform
|
|
10
|
+
|
|
11
|
+
import nbtlib
|
|
12
|
+
import questionary
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.logging import RichHandler
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level="WARNING",
|
|
18
|
+
format="%(message)s",
|
|
19
|
+
datefmt="[%X]",
|
|
20
|
+
handlers=[RichHandler(rich_tracebacks=True)],
|
|
21
|
+
)
|
|
22
|
+
log = logging.getLogger("mc-creative-clone")
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_prism_path() -> Path:
|
|
27
|
+
"""Detects the OS and returns the PrismLauncher data directory.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path: The path to the PrismLauncher data directory.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the current platform is not supported.
|
|
34
|
+
FileNotFoundError: If the PrismLauncher data directory does not exist.
|
|
35
|
+
"""
|
|
36
|
+
# Detect OS
|
|
37
|
+
if platform == "linux" or platform == "linux2":
|
|
38
|
+
# OS is Linux
|
|
39
|
+
prism_path = Path("~/.local/share/PrismLauncher").expanduser()
|
|
40
|
+
elif platform == "darwin":
|
|
41
|
+
# OS is MacOS
|
|
42
|
+
prism_path = Path("~/Library/Application Support/PrismLauncher").expanduser()
|
|
43
|
+
elif platform == "win32":
|
|
44
|
+
# OS is Windows
|
|
45
|
+
prism_path = Path("~\\AppData\\Roaming\\PrismLauncher").expanduser()
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"{platform} is invalid")
|
|
48
|
+
|
|
49
|
+
if prism_path.exists():
|
|
50
|
+
return prism_path
|
|
51
|
+
raise FileNotFoundError(f"{prism_path} does not exist")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_prism_instance(prism_path: Path, instance_name: str | None = None) -> Path:
|
|
55
|
+
"""Finds and returns the path of a PrismLauncher instance.
|
|
56
|
+
|
|
57
|
+
If multiple instances are found, the user is prompted to select one.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
prism_path: The path to the PrismLauncher data directory.
|
|
61
|
+
instance_name: The name of the instance to select. If None, prompts interactively.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Path: The path to the selected PrismLauncher instance.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
FileNotFoundError: If the instances directory or no instances are found.
|
|
68
|
+
"""
|
|
69
|
+
instances_path = prism_path / "instances"
|
|
70
|
+
|
|
71
|
+
if not instances_path.is_dir():
|
|
72
|
+
raise FileNotFoundError(f"{instances_path} is not a valid directory")
|
|
73
|
+
|
|
74
|
+
instances_list = [entry for entry in scandir(instances_path) if entry.is_dir()]
|
|
75
|
+
|
|
76
|
+
if instance_name is not None:
|
|
77
|
+
match = next((i for i in instances_list if i.name == instance_name), None)
|
|
78
|
+
if match is None:
|
|
79
|
+
raise FileNotFoundError(f"Could not find instance '{instance_name}'.")
|
|
80
|
+
return Path(match.path)
|
|
81
|
+
|
|
82
|
+
if len(instances_list) == 0:
|
|
83
|
+
raise FileNotFoundError("Could not find any instances.")
|
|
84
|
+
|
|
85
|
+
if len(instances_list) == 1:
|
|
86
|
+
return Path(instances_list[0].path)
|
|
87
|
+
choices = [
|
|
88
|
+
questionary.Choice(title=instance.name, value=instance.path)
|
|
89
|
+
for instance in instances_list
|
|
90
|
+
]
|
|
91
|
+
selection = questionary.select("Choose an instance", choices=choices).ask()
|
|
92
|
+
if selection is None:
|
|
93
|
+
raise KeyboardInterrupt("User cancelled selection.")
|
|
94
|
+
return Path(selection)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_args() -> argparse.Namespace:
|
|
98
|
+
"""Parses and returns command line arguments.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
argparse.Namespace: The parsed command line arguments.
|
|
102
|
+
"""
|
|
103
|
+
parser = argparse.ArgumentParser(
|
|
104
|
+
prog="mc-creative-clone",
|
|
105
|
+
description="Copies a minecraft world and converts it to a creative mode world.",
|
|
106
|
+
epilog="If no options are provided, the script will run interactively.",
|
|
107
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--prism-path",
|
|
112
|
+
"-p",
|
|
113
|
+
default=None,
|
|
114
|
+
metavar="PATH",
|
|
115
|
+
help="Path to the PrismLauncher data directory. "
|
|
116
|
+
"Defaults to the standard OS path if not specified.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--instance",
|
|
120
|
+
"-i",
|
|
121
|
+
default=None,
|
|
122
|
+
metavar="INSTANCE",
|
|
123
|
+
help="Name of the PrismLauncher instance to use."
|
|
124
|
+
"Prompted interactively if not specified.",
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--world",
|
|
128
|
+
"-w",
|
|
129
|
+
default=None,
|
|
130
|
+
metavar="WORLD",
|
|
131
|
+
help="Name of the world to copy. Prompted interactively if not specified.",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--force",
|
|
135
|
+
"-f",
|
|
136
|
+
action="store_true",
|
|
137
|
+
help="Overwrite the destination world if it already exists, without prompting.",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--verbose",
|
|
141
|
+
"-v",
|
|
142
|
+
action="store_true",
|
|
143
|
+
help="Enable verbose (debug) logging output.",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--dry-run",
|
|
147
|
+
action="store_true",
|
|
148
|
+
help="Preview actions without making any changes to the filesystem.",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
args = parser.parse_args()
|
|
152
|
+
|
|
153
|
+
if args.prism_path is None:
|
|
154
|
+
args.prism_path = get_prism_path()
|
|
155
|
+
|
|
156
|
+
return args
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_minecraft_folder(instance_path: Path) -> Path:
|
|
160
|
+
"""Finds and returns the .minecraft directory for a given instance.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
instance_path: The path of the PrismLauncher instance.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Path: The path to the .minecraft directory.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
FileNotFoundError: If the .minecraft directory cannot be found.
|
|
170
|
+
"""
|
|
171
|
+
instance_minecraft_dir = instance_path / "minecraft"
|
|
172
|
+
if not instance_minecraft_dir.is_dir():
|
|
173
|
+
instance_minecraft_dir = instance_path / ".minecraft"
|
|
174
|
+
if not instance_minecraft_dir.is_dir():
|
|
175
|
+
raise FileNotFoundError(
|
|
176
|
+
f"Could not find minecraft directory in {instance_path}"
|
|
177
|
+
)
|
|
178
|
+
return instance_minecraft_dir
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_save_folder(minecraft_folder_path: Path, world_name: str | None = None) -> Path:
|
|
182
|
+
"""Finds and returns the path of a Minecraft world save folder.
|
|
183
|
+
|
|
184
|
+
If multiple worlds are found, the user is prompted to select one.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
minecraft_folder_path: The path of the PrismLauncher instance .minecraft folder.
|
|
188
|
+
world_name: The name of the world to select. If None, prompts interactively.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Path: The path to the selected world save folder.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
FileNotFoundError: If the saves directory or no save folders are found.
|
|
195
|
+
"""
|
|
196
|
+
instance_save_path = minecraft_folder_path / "saves"
|
|
197
|
+
if not instance_save_path.is_dir():
|
|
198
|
+
raise FileNotFoundError(f"Could not find saves path in {minecraft_folder_path}")
|
|
199
|
+
|
|
200
|
+
save_folders = [entry for entry in scandir(instance_save_path) if entry.is_dir()]
|
|
201
|
+
|
|
202
|
+
if world_name is not None:
|
|
203
|
+
match = next((w for w in save_folders if w.name == world_name), None)
|
|
204
|
+
if match is None:
|
|
205
|
+
raise FileNotFoundError(f"Could not find world '{world_name}'.")
|
|
206
|
+
return Path(match.path)
|
|
207
|
+
|
|
208
|
+
if len(save_folders) == 0:
|
|
209
|
+
raise FileNotFoundError(
|
|
210
|
+
f"Could not find any save folders in {instance_save_path}."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if len(save_folders) == 1:
|
|
214
|
+
return Path(save_folders[0].path)
|
|
215
|
+
choices = [
|
|
216
|
+
questionary.Choice(title=world.name, value=world.path) for world in save_folders
|
|
217
|
+
]
|
|
218
|
+
selection = questionary.select("Choose a world to copy", choices=choices).ask()
|
|
219
|
+
if selection is None:
|
|
220
|
+
raise KeyboardInterrupt("User cancelled selection.")
|
|
221
|
+
return Path(selection)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def get_level_dat(world_path: Path) -> Path:
|
|
225
|
+
"""Finds and returns the path of a world's level.dat file.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
world_path: The path to the world save folder.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Path: The path to the level.dat file.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
FileNotFoundError: If the level.dat file does not exist.
|
|
235
|
+
"""
|
|
236
|
+
level_dat_path = world_path / "level.dat"
|
|
237
|
+
|
|
238
|
+
if not level_dat_path.is_file():
|
|
239
|
+
raise FileNotFoundError(f"{level_dat_path} is not a valid filepath.")
|
|
240
|
+
|
|
241
|
+
return level_dat_path
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_creative_world_path(world_path: Path) -> Path:
|
|
245
|
+
"""Generates the path for the creative backup world.
|
|
246
|
+
|
|
247
|
+
Appends '_creative' and the current date to the original world path.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
world_path: The path to the original world save folder.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Path: The path to the creative backup world.
|
|
254
|
+
"""
|
|
255
|
+
return Path(str(world_path) + f"_creative_{date.today()}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def copy_world(world_path: Path, force: bool = False) -> Path:
|
|
259
|
+
"""Copies a world folder and returns the path of the new copy.
|
|
260
|
+
|
|
261
|
+
The world folder is named with the original name suffixed by
|
|
262
|
+
'_creative' and the current date.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
world_path: The path to the world save folder to copy.
|
|
266
|
+
force: If True, overwrites the destination without prompting.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Path: The path to the newly created world copy.
|
|
270
|
+
"""
|
|
271
|
+
new_path = get_creative_world_path(world_path)
|
|
272
|
+
|
|
273
|
+
if new_path.exists():
|
|
274
|
+
if force:
|
|
275
|
+
log.debug(f"Removing {new_path}...")
|
|
276
|
+
rmtree(new_path)
|
|
277
|
+
else:
|
|
278
|
+
log.warning(f"{new_path} already exists")
|
|
279
|
+
overwrite = None
|
|
280
|
+
while overwrite is None:
|
|
281
|
+
overwrite = questionary.confirm(
|
|
282
|
+
f"{new_path} already exists. Overwrite?"
|
|
283
|
+
).ask()
|
|
284
|
+
if overwrite:
|
|
285
|
+
log.debug(f"Removing {new_path}...")
|
|
286
|
+
rmtree(new_path)
|
|
287
|
+
else:
|
|
288
|
+
numbers = [
|
|
289
|
+
int(m.group(1))
|
|
290
|
+
for path in scandir(new_path.parent)
|
|
291
|
+
if path.is_dir()
|
|
292
|
+
and (
|
|
293
|
+
m := re.search(
|
|
294
|
+
rf"{re.escape(new_path.name)}_old_(\d+)$",
|
|
295
|
+
path.name,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
next_number = max(numbers, default=0) + 1
|
|
301
|
+
|
|
302
|
+
backup_path = new_path.parent / f"{new_path.name}_old_{next_number}"
|
|
303
|
+
log.debug(f"Renaming {new_path} to {backup_path}...")
|
|
304
|
+
move(new_path, backup_path)
|
|
305
|
+
log.debug(f"Renamed {new_path} to {backup_path}")
|
|
306
|
+
|
|
307
|
+
log.debug(f"Copying {world_path} to {new_path}...")
|
|
308
|
+
copytree(world_path, new_path)
|
|
309
|
+
log.debug(f"Copied {world_path} to {new_path}")
|
|
310
|
+
return new_path
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def patch_level_dat(level_dat_path: Path) -> None:
|
|
314
|
+
"""Patches a level.dat file to enable creative mode and cheats.
|
|
315
|
+
|
|
316
|
+
Sets GameType to Creative, enables cheats, and updates the world
|
|
317
|
+
and player game modes.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
level_dat_path: The path to the level.dat file to patch.
|
|
321
|
+
"""
|
|
322
|
+
nbt_file = nbtlib.load(level_dat_path)
|
|
323
|
+
nbt_data = nbt_file["Data"]
|
|
324
|
+
|
|
325
|
+
level_name = nbt_data["LevelName"]
|
|
326
|
+
|
|
327
|
+
nbt_data["allowCommands"] = nbtlib.Byte(1)
|
|
328
|
+
nbt_data["GameType"] = nbtlib.Int(1)
|
|
329
|
+
nbt_data["LevelName"] = nbtlib.String(level_name + "_creative")
|
|
330
|
+
if "Player" in nbt_data:
|
|
331
|
+
nbt_data["Player"]["playerGameType"] = nbtlib.Int(1)
|
|
332
|
+
|
|
333
|
+
nbt_file.save()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def launch_prism(prism_path: Path, instance_path: Path) -> None:
|
|
337
|
+
"""Launches PrismLauncher with the specified instance.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
prism_path: The path to the PrismLauncher data directory.
|
|
341
|
+
instance_path: The path to the PrismLauncher instance to launch.
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
FileNotFoundError: If the PrismLauncher executable cannot be found.
|
|
345
|
+
"""
|
|
346
|
+
prism_executable = which("prismlauncher")
|
|
347
|
+
if prism_executable is None:
|
|
348
|
+
raise FileNotFoundError("Could not find prismlauncher executable in PATH.")
|
|
349
|
+
|
|
350
|
+
run( # noqa: S603
|
|
351
|
+
[
|
|
352
|
+
prism_executable,
|
|
353
|
+
"--directory",
|
|
354
|
+
str(prism_path),
|
|
355
|
+
"--launch",
|
|
356
|
+
instance_path.name,
|
|
357
|
+
],
|
|
358
|
+
check=True,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main() -> None:
|
|
363
|
+
"""Main entry point for mc-creative-clone."""
|
|
364
|
+
try:
|
|
365
|
+
args = parse_args()
|
|
366
|
+
log.setLevel(logging.DEBUG if args.verbose else logging.INFO)
|
|
367
|
+
log.info("Hello from mc-creative-clone!")
|
|
368
|
+
|
|
369
|
+
instance = get_prism_instance(args.prism_path, args.instance)
|
|
370
|
+
log.debug(f"Instance path: {instance}")
|
|
371
|
+
|
|
372
|
+
minecraft_folder = get_minecraft_folder(instance)
|
|
373
|
+
log.debug(f"Minecraft folder: {minecraft_folder}")
|
|
374
|
+
|
|
375
|
+
world = get_save_folder(minecraft_folder, args.world)
|
|
376
|
+
log.debug(f"World path: {world}")
|
|
377
|
+
|
|
378
|
+
new_world = get_creative_world_path(world)
|
|
379
|
+
if args.dry_run:
|
|
380
|
+
log.info(f"[DRY RUN] Would make a copy of {world.name}")
|
|
381
|
+
log.info(f"[DRY RUN] Would patch level.dat at {new_world / 'level.dat'}")
|
|
382
|
+
else:
|
|
383
|
+
log.info(f"Making a copy of {world.name}...")
|
|
384
|
+
new_world = copy_world(world, args.force)
|
|
385
|
+
log.info(f"Copied {world.name}")
|
|
386
|
+
|
|
387
|
+
new_level_dat = get_level_dat(new_world)
|
|
388
|
+
log.debug(f"Level.dat path: {new_level_dat}")
|
|
389
|
+
log.debug(f"Patching {new_level_dat}...")
|
|
390
|
+
patch_level_dat(new_level_dat)
|
|
391
|
+
log.debug(f"Patched {new_level_dat}")
|
|
392
|
+
|
|
393
|
+
log.info("Launching Minecraft through PrismLauncher...")
|
|
394
|
+
launch_prism(args.prism_path, instance)
|
|
395
|
+
|
|
396
|
+
log.info("Done!")
|
|
397
|
+
except KeyboardInterrupt:
|
|
398
|
+
log.info("Aborted")
|
|
399
|
+
exit(0)
|
|
400
|
+
except FileNotFoundError:
|
|
401
|
+
log.exception("Hit a FileNotFoundError.")
|
|
402
|
+
exit(1)
|
|
403
|
+
except ValueError:
|
|
404
|
+
log.exception("Hit a ValueError.")
|
|
405
|
+
exit(1)
|
|
406
|
+
except CalledProcessError:
|
|
407
|
+
log.exception("Hit a CalledProcessError.")
|
|
408
|
+
exit(1)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
if __name__ == "__main__":
|
|
412
|
+
main()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mc-creative-clone
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Copies a Minecraft world and converts it to a creative mode backup
|
|
5
|
+
Project-URL: Repository, https://github.com/xanderboy2001/mc-creative-clone
|
|
6
|
+
Author-email: Alexander Christian <alexanderechristian@gmail.com>
|
|
7
|
+
License: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: minecraft,nbt,prismlauncher
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Games/Entertainment
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: nbtlib>=2.0.4
|
|
17
|
+
Requires-Dist: questionary>=2.1.1
|
|
18
|
+
Requires-Dist: rich>=14.3.3
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<!-- Improved compatibility of back to top link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
|
|
22
|
+
<a id="readme-top"></a>
|
|
23
|
+
<!--
|
|
24
|
+
*** Thanks for checking out the Best-README-Template. If you have a suggestion
|
|
25
|
+
*** that would make this better, please fork the repo and create a pull request
|
|
26
|
+
*** or simply open an issue with the tag "enhancement".
|
|
27
|
+
*** Don't forget to give the project a star!
|
|
28
|
+
*** Thanks again! Now go create something AMAZING! :D
|
|
29
|
+
-->
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<!-- PROJECT SHIELDS -->
|
|
34
|
+
<!--
|
|
35
|
+
*** I'm using markdown "reference style" links for readability.
|
|
36
|
+
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
|
|
37
|
+
*** See the bottom of this document for the declaration of the reference variables
|
|
38
|
+
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
|
39
|
+
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
|
40
|
+
-->
|
|
41
|
+
[![Contributors][contributors-shield]][contributors-url]
|
|
42
|
+
[![Forks][forks-shield]][forks-url]
|
|
43
|
+
[![Stargazers][stars-shield]][stars-url]
|
|
44
|
+
[![Issues][issues-shield]][issues-url]
|
|
45
|
+
[![GPL-3.0-or-later][license-shield]][license-url]
|
|
46
|
+
[![LinkedIn][linkedin-shield]][linkedin-url]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
<h3 align="center">Minecraft Creative Backup</h3>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
A command-line tool that copies a Minecraft world save, patches it into creative mode with cheats enabled, and launches it through PrismLauncher — all in one command.
|
|
54
|
+
<br />
|
|
55
|
+
<a href="https://github.com/xanderboy2001/mc-creative-clone"><strong>Explore the docs »</strong></a>
|
|
56
|
+
<br />
|
|
57
|
+
<br />
|
|
58
|
+
<a href="https://github.com/xanderboy2001/mc-creative-clone">View Demo</a>
|
|
59
|
+
·
|
|
60
|
+
<a href="https://github.com/xanderboy2001/mc-creative-clone/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
|
61
|
+
·
|
|
62
|
+
<a href="https://github.com/xanderboy2001/mc-creative-clone/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
<!-- TABLE OF CONTENTS -->
|
|
69
|
+
<details>
|
|
70
|
+
<summary>Table of Contents</summary>
|
|
71
|
+
<ol>
|
|
72
|
+
<li>
|
|
73
|
+
<a href="#about-the-project">About The Project</a>
|
|
74
|
+
<ul>
|
|
75
|
+
<li><a href="#built-with">Built With</a></li>
|
|
76
|
+
</ul>
|
|
77
|
+
</li>
|
|
78
|
+
<li>
|
|
79
|
+
<a href="#getting-started">Getting Started</a>
|
|
80
|
+
<ul>
|
|
81
|
+
<li><a href="#prerequisites">Prerequisites</a></li>
|
|
82
|
+
<li><a href="#installation">Installation</a></li>
|
|
83
|
+
</ul>
|
|
84
|
+
</li>
|
|
85
|
+
<li>
|
|
86
|
+
<a href="#usage">Usage</a>
|
|
87
|
+
<ul>
|
|
88
|
+
<li><a href="#options">Options></a></li>
|
|
89
|
+
<li><a href="#examples"><Examples></a></li>
|
|
90
|
+
</ul>
|
|
91
|
+
</li>
|
|
92
|
+
<li><a href="#contributing">Contributing</a></li>
|
|
93
|
+
<li><a href="#license">License</a></li>
|
|
94
|
+
<li><a href="#contact">Contact</a></li>
|
|
95
|
+
</ol>
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
<!-- ABOUT THE PROJECT -->
|
|
101
|
+
## About The Project
|
|
102
|
+
|
|
103
|
+
Minecraft Creative Backup is a Python CLI tool that automates the process of creating creative mode backups of Minecraft worlds managed by PrismLauncher.
|
|
104
|
+
|
|
105
|
+
When building or experimenting in Minecraft, it's useful to have a creative mode copy of a survival world - but doing it manually means copying the folder, opening the world, changing the game mode, and enabling cheats. This tool automates all of that in a single command.
|
|
106
|
+
|
|
107
|
+
### What it does
|
|
108
|
+
|
|
109
|
+
- Copies a Minecraft world save folder
|
|
110
|
+
- Patches the world's `level.dat` to enable creative mode and cheats
|
|
111
|
+
- Renames the backup with the current date for easy identification
|
|
112
|
+
- Launches the instance directly through PrismLauncher
|
|
113
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
### Built With
|
|
118
|
+
|
|
119
|
+
* [![Python][Python-badge]][Python-url]
|
|
120
|
+
* [![Rich][Rich-badge]][Rich-url]
|
|
121
|
+
* [![questionary][questionary-badge]][questionary-url]
|
|
122
|
+
* [![nbtlib][nbtlib-badge]][nbtlib-url]
|
|
123
|
+
|
|
124
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
<!-- GETTING STARTED -->
|
|
129
|
+
## Getting Started
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### Prerequisites
|
|
133
|
+
|
|
134
|
+
- Python 3.14 or higher
|
|
135
|
+
- [PrismLauncher](https://prismlauncher.org/) installed and configured with at least one instance and world.
|
|
136
|
+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip
|
|
137
|
+
|
|
138
|
+
### Installation
|
|
139
|
+
|
|
140
|
+
1. Clone the repository
|
|
141
|
+
```sh
|
|
142
|
+
git clone https://github.com/xanderboy2001/mc-creative-clone.git
|
|
143
|
+
cd mc-creative-clone
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. Install the package
|
|
147
|
+
```sh
|
|
148
|
+
uv tool install .
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or with pip:
|
|
152
|
+
```sh
|
|
153
|
+
pip install .
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
3. Verify the installation
|
|
157
|
+
```sh
|
|
158
|
+
mc-creative-clone --help
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
<!-- USAGE EXAMPLES -->
|
|
167
|
+
## Usage
|
|
168
|
+
|
|
169
|
+
Run the tool interactively - you will be prompted to select an instance and world:
|
|
170
|
+
```sh
|
|
171
|
+
mc-creative-clone
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or specify options directly to skip the interactive prompts
|
|
175
|
+
```sh
|
|
176
|
+
mc-creative-clone --instance "My instance" --world "My world"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Options
|
|
180
|
+
|
|
181
|
+
| **Option** | **Short** | **Description** |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `--prism-path PATH` | `-p` | Path to PrismLauncher data directory. Defaults to the standard OS path. |
|
|
184
|
+
| `--instance INSTANCE` | `-i` | Name of the PrismLauncher instance to use. |
|
|
185
|
+
| `--world WORLD` | `-w` | Name of the world to copy. |
|
|
186
|
+
| `--force` | `-f` | Overwrite the destination world if it already exists without prompting. |
|
|
187
|
+
| `--dry-run` | | Preview actions without making any changes to the filesystem. |
|
|
188
|
+
| `--verbose` | `-v` | Enable verbose debug logging output. |
|
|
189
|
+
|
|
190
|
+
### Examples
|
|
191
|
+
|
|
192
|
+
Preview what would happen without making any changes:
|
|
193
|
+
```sh
|
|
194
|
+
mc-creative-clone --dry-run --instance "Survival" --world "My World"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
Force overwrite an existing backup:
|
|
199
|
+
```sh
|
|
200
|
+
mc-creative-clone --force --instance "Survival" --world "My World"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
Use a custom PrismLauncher data directory:
|
|
205
|
+
```sh
|
|
206
|
+
mc-creative-clone --prism-path "/path/to/prismlauncher"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
<!-- CONTRIBUTING -->
|
|
214
|
+
## Contributing
|
|
215
|
+
|
|
216
|
+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
|
217
|
+
|
|
218
|
+
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
|
219
|
+
Don't forget to give the project a star! Thanks again!
|
|
220
|
+
|
|
221
|
+
1. Fork the Project
|
|
222
|
+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
|
|
223
|
+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
|
|
224
|
+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
|
|
225
|
+
5. Open a Pull Request
|
|
226
|
+
|
|
227
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
228
|
+
|
|
229
|
+
### Top contributors:
|
|
230
|
+
|
|
231
|
+
<a href="https://github.com/xanderboy2001/mc-creative-clone/graphs/contributors">
|
|
232
|
+
<img src="https://contrib.rocks/image?repo=xanderboy2001/mc-creative-clone" alt="contrib.rocks image" />
|
|
233
|
+
</a>
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
<!-- LICENSE -->
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
Distributed under the GPL-3.0-or-later. See `LICENSE.txt` for more information.
|
|
241
|
+
|
|
242
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
<!-- CONTACT -->
|
|
247
|
+
## Contact
|
|
248
|
+
|
|
249
|
+
Alexander Christian - alexanderechristian@gmail.com
|
|
250
|
+
|
|
251
|
+
Project Link: [https://github.com/xanderboy2001/mc-creative-clone](https://github.com/xanderboy2001/mc-creative-clone)
|
|
252
|
+
|
|
253
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
<!-- MARKDOWN LINKS & IMAGES -->
|
|
258
|
+
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
|
259
|
+
[contributors-shield]: https://img.shields.io/github/contributors/xanderboy2001/mc-creative-clone.svg?style=for-the-badge
|
|
260
|
+
[contributors-url]: https://github.com/xanderboy2001/mc-creative-clone/graphs/contributors
|
|
261
|
+
[forks-shield]: https://img.shields.io/github/forks/xanderboy2001/mc-creative-clone.svg?style=for-the-badge
|
|
262
|
+
[forks-url]: https://github.com/xanderboy2001/mc-creative-clone/network/members
|
|
263
|
+
[stars-shield]: https://img.shields.io/github/stars/xanderboy2001/mc-creative-clone.svg?style=for-the-badge
|
|
264
|
+
[stars-url]: https://github.com/xanderboy2001/mc-creative-clone/stargazers
|
|
265
|
+
[issues-shield]: https://img.shields.io/github/issues/xanderboy2001/mc-creative-clone.svg?style=for-the-badge
|
|
266
|
+
[issues-url]: https://github.com/xanderboy2001/mc-creative-clone/issues
|
|
267
|
+
[license-shield]: https://img.shields.io/github/license/xanderboy2001/mc-creative-clone.svg?style=for-the-badge
|
|
268
|
+
[license-url]: https://github.com/xanderboy2001/mc-creative-clone/blob/main/LICENSE
|
|
269
|
+
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
|
270
|
+
[linkedin-url]: https://linkedin.com/in/alexander-e-christian
|
|
271
|
+
[product-screenshot]: images/screenshot.png
|
|
272
|
+
<!-- Shields.io badges. You can a comprehensive list with many more badges at: https://github.com/inttter/md-badges -->
|
|
273
|
+
[Python-badge]: https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white
|
|
274
|
+
[Python-url]: https://python.org
|
|
275
|
+
[Rich-badge]: https://img.shields.io/badge/Rich-FAD000?style=for-the-badge&logo=python&logoColor=black
|
|
276
|
+
[Rich-url]: https://github.com/Textualize/rich
|
|
277
|
+
[questionary-badge]: https://img.shields.io/badge/questionary-4a4a55?style=for-the-badge&logo=python&logoColor=white
|
|
278
|
+
[questionary-url]: https://github.com/tmbo/questionary
|
|
279
|
+
[nbtlib-badge]: https://img.shields.io/badge/nbtlib-4a4a55?style=for-the-badge&logo=python&logoColor=white
|
|
280
|
+
[nbtlib-url]: https://github.com/vberlier/nbtlib
|