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.
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("mc-creative-clone")
@@ -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
+ &middot;
60
+ <a href="https://github.com/xanderboy2001/mc-creative-clone/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
61
+ &middot;
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