Skip to content

Instantly share code, notes, and snippets.

@StoneLabs
Created May 23, 2026 18:44
Show Gist options
  • Select an option

  • Save StoneLabs/68d3fb02d84d19bd946ebf71bbf0da07 to your computer and use it in GitHub Desktop.

Select an option

Save StoneLabs/68d3fb02d84d19bd946ebf71bbf0da07 to your computer and use it in GitHub Desktop.
Minecraft skyblock mca world clearing script
import os
import sys
import struct
import zlib
import gzip
import tempfile
import io
import math
from nbt import nbt
# ---------- Configuration ----------
SPAWN_X = 0
SPAWN_Z = 0
DISTANCE_THRESHOLD = 480 # blocks
SECTOR_SIZE = 4096
# -----------------------------------
def read_region_header(file_path):
"""Reads location & timestamp tables from .mca file."""
locations, timestamps = [], []
with open(file_path, 'rb') as f:
loc_data = f.read(4096)
if len(loc_data) < 4096:
raise ValueError("File too short for location table")
for i in range(1024):
entry = struct.unpack('>I', loc_data[i*4:(i+1)*4])[0]
locations.append(((entry >> 8) & 0xFFFFFF, entry & 0xFF))
ts_data = f.read(4096)
if len(ts_data) < 4096:
raise ValueError("File too short for timestamp table")
for i in range(1024):
timestamps.append(struct.unpack('>I', ts_data[i*4:(i+1)*4])[0])
return locations, timestamps
def read_chunk_data(file_path, sector_offset, sector_count):
if sector_offset == 0 or sector_count == 0:
return None
with open(file_path, 'rb') as f:
f.seek(sector_offset * SECTOR_SIZE)
length = struct.unpack('>I', f.read(4))[0]
compression = struct.unpack('b', f.read(1))[0]
if compression == 2:
return zlib.decompress(f.read(length - 1))
elif compression == 1:
print(" [WARN] GZip compression – chunk skipped")
return None
else:
print(f" [WARN] Unknown compression {compression} – chunk skipped")
return None
def chunk_distance(x_pos, z_pos):
# Use chunk center in block coords for a fair distance check.
# Corner (x_pos * 16) would miss the far half of boundary chunks.
cx = x_pos * 16 + 8
cz = z_pos * 16 + 8
return math.sqrt(cx ** 2 + cz ** 2)
def get_block_range(x_pos, z_pos):
return (x_pos * 16, z_pos * 16), (x_pos * 16 + 15, z_pos * 16 + 15)
def create_zero_heightmaps():
"""
Modern (1.18+) Heightmaps: a compound containing a
MOTION_BLOCKING long array of 37 zeros (16x16 area).
"""
hm_compound = nbt.TAG_Compound(name='Heightmaps')
mot_array = nbt.TAG_Long_Array(name='MOTION_BLOCKING')
mot_array.value = [0] * 37
hm_compound.tags.append(mot_array)
return hm_compound
_debug_printed = False
def _get_sections(level):
"""
FIX: In 1.18+ the sections list is 'sections' (lowercase).
In 1.17 and below it was 'Sections' under the Level compound.
Try both so the script works regardless of world version.
"""
if 'sections' in level:
return level['sections']
if 'Sections' in level:
return level['Sections']
return []
def clear_chunk(chunk_nbt):
global _debug_printed
level = chunk_nbt.get('Level', chunk_nbt)
# Debug output (first non-empty chunk only)
if not _debug_printed:
sections = _get_sections(level)
if len(sections) > 0:
try:
sec0 = sections[0]
if 'block_states' in sec0 and 'palette' in sec0['block_states']:
pal0 = sec0['block_states']['palette']
names = [entry['Name'].value for entry in pal0]
print(f" [DEBUG] First palette entries before: {names[:5]}...")
else:
print(" [DEBUG] No palette in first section")
except Exception:
print(" [DEBUG] Could not read initial palette")
else:
print(" [DEBUG] Chunk has no sections (already empty)")
_debug_printed = True
level['Status'] = nbt.TAG_String(name='Status', value='minecraft:full')
# FIX: Use _get_sections() to handle both 'sections' (1.18+) and 'Sections' (1.17-)
for section in _get_sections(level):
air_block = nbt.TAG_Compound()
air_block.tags.append(nbt.TAG_String(name='Name', value='minecraft:air'))
palette = nbt.TAG_List(name='palette', type=nbt.TAG_Compound)
palette.tags.append(air_block)
if 'block_states' not in section:
section['block_states'] = nbt.TAG_Compound(name='block_states')
bs = section['block_states']
bs['palette'] = palette
if 'data' in bs:
del bs['data']
for tag_name in ('BlockLight', 'SkyLight'):
if tag_name in section:
del section[tag_name]
# Remove all metadata that references non-air blocks.
# FIX: Include lowercase 1.18+ names for 'lights' and 'post_processing'
for tag in ('block_entities', 'block_ticks', 'fluid_ticks',
'entities', 'Lights', 'lights', 'PostProcessing', 'post_processing'):
if tag in level:
del level[tag]
# Correct Heightmaps: a compound with MOTION_BLOCKING = zeros
level['Heightmaps'] = create_zero_heightmaps()
if not _debug_printed:
_debug_printed = True
sections = _get_sections(level)
if len(sections) > 0:
try:
sec0 = sections[0]
if 'block_states' in sec0 and 'palette' in sec0['block_states']:
pal0 = sec0['block_states']['palette']
names = [entry['Name'].value for entry in pal0]
print(f" [DEBUG] First palette entries after: {names}")
except Exception:
print(" [DEBUG] Could not read final palette")
def build_chunk_payload(chunk_nbt):
"""
Serialise chunk NBT to a Zlib-compressed region entry.
Writes to a temporary GZip file, decompresses to get raw NBT,
then Zlib-compresses (Minecraft's region format).
"""
with tempfile.NamedTemporaryFile(delete=False, suffix='.nbt') as tmp:
tmp_name = tmp.name
try:
chunk_nbt.write_file(tmp_name) # GZip compressed file
with gzip.open(tmp_name, 'rb') as f:
raw_nbt = f.read() # uncompressed NBT
finally:
os.unlink(tmp_name)
compressed = zlib.compress(raw_nbt)
# length includes the compression type byte
payload = struct.pack('>I', len(compressed) + 1)
payload += struct.pack('b', 2) # Zlib
payload += compressed
return payload
def update_region_file(file_path, locations, timestamps, chunk_payloads):
new_locs = []
blocks = []
offset = 2 # sectors after headers
for i in range(1024):
if chunk_payloads[i] is not None:
sectors = math.ceil(len(chunk_payloads[i]) / SECTOR_SIZE)
new_locs.append((offset, sectors))
blocks.append(chunk_payloads[i])
offset += sectors
else:
new_locs.append((0, 0))
blocks.append(None)
with open(file_path, 'wb') as f:
for off, cnt in new_locs:
f.write(struct.pack('>I', (off << 8) | (cnt & 0xFF)))
for ts in timestamps:
f.write(struct.pack('>I', ts))
for b in blocks:
if b is not None:
f.write(b)
pad = SECTOR_SIZE - (len(b) % SECTOR_SIZE)
if pad != SECTOR_SIZE:
f.write(b'\x00' * pad)
remaining = SECTOR_SIZE - (f.tell() % SECTOR_SIZE)
if remaining != SECTOR_SIZE:
f.write(b'\x00' * remaining)
def process_region_file(folder, filename):
file_path = os.path.join(folder, filename)
try:
print(f" Processing {filename}...")
locs, timestamps = read_region_header(file_path)
payloads = [None] * 1024
modified = False
checked = cleared = 0
for i in range(1024):
off, cnt = locs[i]
if off == 0 and cnt == 0:
continue
raw = read_chunk_data(file_path, off, cnt)
if raw is None:
continue
try:
chunk = nbt.NBTFile(buffer=io.BytesIO(raw))
level = chunk.get('Level', chunk)
x = level['xPos'].value
z = level['zPos'].value
dist = chunk_distance(x, z)
if dist > DISTANCE_THRESHOLD:
clear_chunk(chunk)
payloads[i] = build_chunk_payload(chunk)
cleared += 1
modified = True
else:
payloads[i] = build_chunk_payload(chunk)
checked += 1
except Exception as e:
print(f" [ERROR] chunk {i}: {e}")
if raw:
try:
payloads[i] = build_chunk_payload(
nbt.NBTFile(buffer=io.BytesIO(raw)))
except Exception:
pass
if modified:
update_region_file(file_path, locs, timestamps, payloads)
new_size = os.path.getsize(file_path)
print(f" => File size after update: {new_size} bytes")
print(f" => Updated {filename} ({cleared}/{checked} chunks cleared)")
else:
print(f" => No chunks needed clearing ({checked} checked)")
except Exception as e:
print(f" !! Failed to process {filename}: {e} – skipping file")
def main():
if len(sys.argv) < 2:
print("Usage: python clear_distant_chunks.py <region_folder>")
sys.exit(1)
folder = sys.argv[1]
if not os.path.isdir(folder):
print(f"'{folder}' is not a directory.")
sys.exit(1)
files = sorted(f for f in os.listdir(folder) if f.endswith('.mca'))
total_files = len(files)
if not files:
print("No .mca files found.")
return
print(f"Found {total_files} region files. Threshold: {DISTANCE_THRESHOLD} blocks "
f"from spawn ({SPAWN_X}, {SPAWN_Z})\n")
for i, f in enumerate(files, 1):
percentage = (i / total_files) * 100
print(f"[{i}/{total_files} - {percentage:.1f}%] {f}")
process_region_file(folder, f)
print()
print("🎉 All done!")
if __name__ == '__main__':
main()
@StoneLabs

Copy link
Copy Markdown
Author

Usage:
-> Generate chunks using a mod (i generate chunky)
-> Run the script against the folder containing the mca files.
-> It will clear anything outside further than DISTANCE_THRESHOLD from (spawn_x, spawn_y). If you want to clear the entire world, set it to 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment