Skip to content

Commit 41e747f

Browse files
authored
Merge pull request #4 from Nyveon/dev
Merging dev branch to main for release 1.4
2 parents 64283c7 + d6b9a62 commit 41e747f

10 files changed

+167
-38
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,7 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# testing stuff
132+
/new_region/
133+
/world/

.idea/.gitignore

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/MCStructureCleaner.iml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/profiles_settings.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+20-15
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,39 @@ Fixes errors such as `Unknown structure start: <missing structure>`, `Failed to
99
# Usage
1010

1111
1. Install the requirements: [Python 3.x](https://www.python.org/) and [Matcool's Anvil Parser](https://github.com/matcool/anvil-parser).
12-
2. Download the [latest release](https://github.com/Nyveon/MCStructureCleaner/releases/) and place main.py in the same directory as your world folder (e.g: if it's a server, in the server folder).
13-
3. Run main.py, and instruct it as to which structure tag you wish to remove. I recommend using [NBTExplorer](https://github.com/jaquadro/NBTExplorer) to find the name.
14-
- -h For help on command line arguments.
15-
- -t For the tag you want removed, in quotes.
16-
- -j For the number of threads you want to run it on. Default: 2 x CPU Cores.
17-
- -w For the name of the world you want to process. Default: "world".
18-
- -r For the name of the sub-folder (dimension) in the world. Default: "".
19-
- Example:
12+
2. Download the [latest release](https://github.com/Nyveon/MCStructureCleaner/releases/) and place `main.py` in the same directory as your world folder
13+
- **Example:** If it's a server: in the server folder, or if it is a singleplayer world, in the saves folder.
14+
3. Run main.py with any of the following configuration properties. I recommend using [NBTExplorer](https://github.com/jaquadro/NBTExplorer) to find the name, or just letting the program fix all non-vanilla names by not inputting any tag.
15+
- `-h` For help on command line arguments.
16+
- `-t` For the tag you want removed, in quotes. Leave empty if you wish to remove ALL NON-VANILLA TAGS.
17+
- `-j` For the number of threads you want to run it on. Default: 2 x CPU Cores.
18+
- `-w` For the name of the world you want to process. Default: "world".
19+
- `-r` For the name of the sub-folder (dimension) in the world. Default: "".
20+
- **Example 1:** This command will delete all non-vanilla structures (defined up to 1.17) in the overworld of the world "SMP"
21+
```
22+
python main.py -w "SMP"
23+
```
24+
- **Example 2:** This command will delete all occurances of "Better Mineshaft" in the world "My World", in the Nether (DIM-1), using 8 threads.
2025
```
2126
python main.py -t "Better Mineshaft" -j 8 -w "My World" -r "DIM-1"
2227
```
23-
- This would remove all references of the tag "Better Mineshaft" from the nether (DIM-1) of the world named My World, using 8 threads.
24-
4. Let it run.
28+
4. Let it run. This may take a while, depending on the power of your computer and the size of your world.
2529
5. Replace the contents of your region folder with the contents of new_region.
30+
6. Enjoy your now working world 😊
2631

2732
# Todo:
2833

2934
- [x] More detailed output.
30-
- [ ] Multiple tag input. (In progress)
35+
- [x] Multiple tag input. (Implemented in 1.4)
3136
- [x] Multithreading. (Thanks DemonInTheCloset!, now 2.8x faster)
3237
- [x] Command line arguments. (Thanks DemonInTheCloset)
3338
- [x] Selection of world/dimensions.
3439
- [ ] Allow for picking up progress where program left off.
3540
- [ ] Checking disk space available.
36-
- [ ] Auto-removal of all non vanilla structures mode.
41+
- [x] Auto-removal of all non vanilla structures mode. (Implemented in 1.4)
3742

3843
# Notes:
3944

40-
- I have only tested this with 1.16 worlds.
41-
- Feel free to message me on twitter if you need help using it.
42-
- With large worlds, it may take a while to process, also depends on your hardware.
45+
- I have only tested this with 1.16 worlds. In theory it should work with all worlds that use the anvil format though.
46+
- Feel free to message me on discord or twitter if you need help using it.
47+
- Why did we make this? To save our own SMP world after uninstalling some mods. We had spent a lot of time on it, and didn't want anyone else to have to lose their world to the same bug.

main.py

+89-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
MC Structure cleaner
33
By: Nyveon and DemonInTheCloset
44
5-
v: 1.3
5+
v: 1.4
66
Modded structure cleaner for minecraft. Removes all references to non-existent
77
structures to allow for clean error logs and chunk saving.
88
"""
@@ -26,70 +26,119 @@
2626
from multiprocessing import Pool, cpu_count
2727

2828
import anvil # anvil-parser by matcool
29-
30-
VERSION = "1.3"
31-
32-
29+
from typing import Set, Tuple
30+
31+
VERSION = "1.4"
32+
33+
# Configuration variables and constants
34+
# Gracias panchito, tomimi y puntito c:
35+
VANILLA_STRUCTURES = {
36+
"bastion_remnant",
37+
"buried_treasure",
38+
"endcity",
39+
"fortress",
40+
"mansion",
41+
"mineshaft",
42+
"monument",
43+
"nether_fossil",
44+
"ocean_ruin",
45+
"pillager_outpost",
46+
"ruined_portal",
47+
"shipwreck",
48+
"stronghold",
49+
"desert_pyramid",
50+
"igloo",
51+
"jungle_pyramid",
52+
"swamp_hut",
53+
"village"
54+
}
55+
56+
57+
# Print separator
3358
def sep():
3459
"""Print separator line"""
3560
print("----------------------------------")
3661

3762

3863
# Removing Tags
39-
def _remove_tags_region(args: tuple[set[str], Path, Path]) -> int:
64+
def _remove_tags_region(args: Tuple[Set[str], Path, Path, str]) -> int:
4065
return remove_tags_region(*args)
4166

4267

43-
def remove_tags_region(to_replace: set[str], src: Path, dst: Path) -> int:
68+
def remove_tags_region(to_replace: Set[str], src: Path, dst: Path, mode: str) -> int:
4469
"""Remove tags in to_replace from the src region
4570
Write changes to dst/src.name"""
4671
start: float = time.perf_counter()
4772
count: int = 0
4873

74+
print("Checking file:", src)
75+
4976
coords = src.name.split(".")
5077
region = anvil.Region.from_file(str(src.resolve()))
5178
new_region = anvil.EmptyRegion(int(coords[1]), int(coords[2]))
79+
removed_tags = set()
80+
81+
# Lambda function for checking if a tag is valid
82+
if mode == "purge":
83+
def check_tag(_tag):
84+
return _tag.name not in VANILLA_STRUCTURES
85+
else:
86+
def check_tag(_tag):
87+
return _tag.name in to_replace
5288

5389
# Check chunks
5490
for chunk_x, chunk_z in it.product(range(32), repeat=2):
5591
# Chunk Exists
5692
if region.chunk_location(chunk_x, chunk_z) != (0, 0):
5793
data = region.chunk_data(chunk_x, chunk_z)
94+
data_copy = region.chunk_data(chunk_x, chunk_z)
5895

5996
for tag in data["Level"]["Structures"]["Starts"].tags:
60-
if tag.name in to_replace:
61-
del data["Level"]["Structures"]["Starts"][tag.name]
97+
if check_tag(tag):
98+
del data_copy["Level"]["Structures"]["Starts"][tag.name]
6299
count += 1
100+
removed_tags.add(tag.name)
63101

64102
for tag in data["Level"]["Structures"]["References"].tags:
65-
if tag.name in to_replace:
66-
del data["Level"]["Structures"]["References"][tag.name]
103+
if check_tag(tag):
104+
del data_copy["Level"]["Structures"]["References"][tag.name]
67105
count += 1
106+
removed_tags.add(tag.name)
68107

69108
# Add the modified chunk data to the new region
70-
new_region.add_chunk(anvil.Chunk(data))
109+
new_region.add_chunk(anvil.Chunk(data_copy))
71110

72111
# Save Region
73112
new_region.save(str((dst / src.name).resolve()))
74113

75114
end: float = time.perf_counter()
76-
print(f"{count} instances of tags removed in {end - start:.3f} s")
115+
print(f"File {src}: {count} instances of tags removed in {end - start:.3f} s")
116+
117+
# Output for purge mode (removed non vanilla tags per file)
118+
if mode == "purge" and len(removed_tags) != 0:
119+
print("Non-vanilla tags found:")
120+
print(removed_tags)
121+
sep()
77122

78123
return count
79124

80125

81-
def remove_tags(tags: set[str], src: Path, dst: Path, jobs: int) -> None:
126+
def remove_tags(tags: Set[str], src: Path, dst: Path, jobs: int, mode: str) -> None:
82127
"""Removes tags from src region files and writes them to dst"""
83128
with Pool(processes=jobs) as pool:
84129
start = time.perf_counter()
85130

86-
data = zip(it.repeat(tags), src.iterdir(), it.repeat(dst))
131+
data = zip(it.repeat(tags), src.iterdir(), it.repeat(dst), it.repeat(mode))
87132
count = sum(pool.map(_remove_tags_region, data))
88133

89134
end = time.perf_counter()
90135

91136
sep()
92-
print(f"Done!\nRemoved {count} instances of tags: {tags}")
137+
if mode == "purge":
138+
print(f"Done!\nRemoved {count} instances of non-vanilla tags")
139+
else:
140+
print(f"Done!\nRemoved {count} instances of tags: {tags}")
141+
93142
print(f"Took {end - start:.3f} seconds")
94143

95144

@@ -99,7 +148,7 @@ def setup_environment(new_region: Path) -> bool:
99148
if new_region.exists():
100149
print(f"{new_region.resolve()} exists, this may cause problems")
101150
proceed = input("Do you want to proceed regardless? [y/N] ")
102-
151+
sep()
103152
return proceed.startswith("y")
104153

105154
new_region.mkdir()
@@ -115,14 +164,14 @@ def get_args() -> Namespace:
115164

116165
prog_msg = f"MC Structure cleaner\nBy: Nyveon\nVersion: {VERSION}"
117166
tag_help = "The EXACT structure tag name you want removed (Use NBTExplorer\
118-
to find the name)"
167+
to find the name), default is an empty string (for use in purge mode)"
119168
jobs_help = f"The number of processes to run (default: {jobs})"
120169
world_help = f"The name of the world you wish to process (default: \"world\")"
121170
region_help = f"The name of the region folder (dimension) you wish to process (default: "")"
122171

123172
parser = ArgumentParser(prog=prog_msg)
124173

125-
parser.add_argument("-t", "--tag", type=str, help=tag_help, required=True)
174+
parser.add_argument("-t", "--tag", type=str, help=tag_help, default="", nargs="*")
126175
parser.add_argument("-j", "--jobs", type=int, help=jobs_help, default=jobs)
127176
parser.add_argument("-w", "--world", type=str, help=world_help, default="world")
128177
parser.add_argument("-r", "--region", type=str, help=region_help, default="")
@@ -133,32 +182,49 @@ def get_args() -> Namespace:
133182
def _main() -> None:
134183
args = get_args()
135184

136-
to_replace = args.tag
185+
to_replace = set(args.tag)
137186
new_region = Path("new_" + args.region + "region")
138187
world_region = Path(args.world + "/" + args.region + "/region")
139188
num_processes = args.jobs
140189

141-
print(f"Replacing {to_replace} in all region files in {world_region}.")
142190
sep()
143191

192+
# Force purge mode if no tag is given, otherwise normal.
193+
mode = "normal"
194+
if args.tag == "":
195+
print("No tag given, will run in purge mode.")
196+
mode = "purge"
197+
else:
198+
print("Tag(s) given, will run in normal mode.")
199+
200+
# Feedback as to what the program is about to do.
201+
if mode == "normal":
202+
print(f"Replacing {to_replace} in all region files in {world_region}.")
203+
elif mode == "purge":
204+
print(f"Replacing all non-vanilla structures in all region files in {world_region}.")
205+
sep()
206+
207+
# Check if world exists
144208
if not world_region.exists():
145209
print(f"Couldn't find {world_region.resolve()}")
146210
return None
147211

212+
# Check if output already exists
148213
if not setup_environment(new_region):
149214
print("Aborted, nothing was done")
150215
return None
151216

152217
n_to_process = len(list(world_region.iterdir()))
153218

154-
remove_tags({to_replace}, world_region, new_region, num_processes)
219+
remove_tags(to_replace, world_region, new_region, num_processes, mode)
155220

221+
# End output
156222
sep()
157223
print(f"Processed {n_to_process} files")
158224
print(f"You can now replace {world_region} with {new_region}")
159-
160225
return None
161226

162227

228+
# When running
163229
if __name__ == "__main__":
164230
_main()

0 commit comments

Comments
 (0)