Guest User

diffing.py

a guest
May 30th, 2026
25
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.52 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import argparse
  4. import pathlib
  5. import re
  6. import signal
  7. import sys
  8.  
  9. import anvil
  10. from anvil.chunk import _states_from_section, bin_append
  11. from anvil.versions import VERSION_20w17a
  12.  
  13. AIR_ID = "minecraft:air"
  14. AIR_IDS = {AIR_ID, "air"}
  15.  
  16.  
  17. def parse_args():
  18.     parser = argparse.ArgumentParser(
  19.         description="Report block coordinates where old region blocks were replaced by air in the new region."
  20.     )
  21.     parser.add_argument("old_mca", help="Old region file path")
  22.     parser.add_argument("new_mca", help="New region file path")
  23.     parser.add_argument(
  24.         "target_substring",
  25.         nargs="?",
  26.         default=None,
  27.         help="Optional substring to filter old block IDs (e.g. pale_oak)",
  28.     )
  29.     return parser.parse_args()
  30.  
  31.  
  32. class SectionDecoder:
  33.     __slots__ = (
  34.         "palette",
  35.         "data",
  36.         "mask",
  37.         "bits",
  38.         "stretches",
  39.         "section_y",
  40.     )
  41.  
  42.     def __init__(self, chunk=None, section=None):
  43.         if section is None:
  44.             self.section_y = None
  45.             self.palette = []
  46.             self.data = []
  47.             self.bits = 4
  48.             self.mask = 0xF
  49.             self.stretches = False
  50.             return
  51.  
  52.         self.section_y = section["Y"].value
  53.         self.palette = self._build_palette(chunk, section)
  54.         self.data = self._build_data(section)
  55.         self.bits = max((len(self.palette) - 1).bit_length(), 4)
  56.         self.mask = (1 << self.bits) - 1
  57.         self.stretches = chunk.version < VERSION_20w17a
  58.  
  59.     @staticmethod
  60.     def _build_palette(chunk, section):
  61.         if "block_states" not in section and "Palette" not in section:
  62.             return []
  63.  
  64.         try:
  65.             raw_palette = chunk.get_palette(section)
  66.         except KeyError:
  67.             return []
  68.  
  69.         if not raw_palette:
  70.             return []
  71.         return [block.id for block in raw_palette]
  72.  
  73.     @staticmethod
  74.     def _build_data(section):
  75.         if "block_states" not in section and "BlockStates" not in section:
  76.             return []
  77.  
  78.         try:
  79.             return _states_from_section(section)
  80.         except KeyError:
  81.             return []
  82.  
  83.     def block_id(self, index):
  84.         if not self.palette or not self.data:
  85.             return AIR_ID
  86.  
  87.         if self.stretches:
  88.             state_index = index * self.bits // 64
  89.             if state_index >= len(self.data):
  90.                 return AIR_ID
  91.             shift = (self.bits * index) % 64
  92.             value = self.data[state_index] >> shift
  93.             if 64 - shift < self.bits:
  94.                 if state_index + 1 >= len(self.data):
  95.                     return AIR_ID
  96.                 value = bin_append(
  97.                     self.data[state_index + 1] & ((1 << (self.bits - shift)) - 1),
  98.                     value,
  99.                     self.bits - shift,
  100.                 )
  101.         else:
  102.             state_index = index // (64 // self.bits)
  103.             if state_index >= len(self.data):
  104.                 return AIR_ID
  105.             shift = (index % (64 // self.bits)) * self.bits
  106.             value = self.data[state_index] >> shift
  107.  
  108.         palette_id = value & self.mask
  109.         result = self.palette[palette_id] if palette_id < len(self.palette) else AIR_ID
  110.         return result.lower() if result is not None else AIR_ID
  111.  
  112.  
  113. def _get_sections(chunk):
  114.     sections = chunk.data.get("sections")
  115.     if not sections:
  116.         sections = chunk.data.get("Sections", [])
  117.     return {section["Y"].value: section for section in sections}
  118.  
  119.  
  120. def compare_chunks(old_chunk, new_chunk, target_substr):
  121.     if getattr(old_chunk, "data", None) == getattr(new_chunk, "data", None):
  122.         return
  123.  
  124.     old_sections = _get_sections(old_chunk)
  125.     new_sections = _get_sections(new_chunk)
  126.  
  127.     for section_y, old_section in old_sections.items():
  128.         new_section = new_sections.get(section_y)
  129.         if new_section is None:
  130.             new_decoder = SectionDecoder()
  131.         else:
  132.             if old_section == new_section:
  133.                 continue
  134.             new_decoder = SectionDecoder(new_chunk, new_section)
  135.  
  136.         old_decoder = SectionDecoder(old_chunk, old_section)
  137.  
  138.         y_base = section_y * 16
  139.         range_z = range(16)
  140.         range_x = range(16)
  141.         for y in range(16):
  142.             row_base = (y * 16) << 4
  143.             world_y = y_base + y
  144.             for z in range_z:
  145.                 cell_base = row_base + (z << 4)
  146.                 for x in range_x:
  147.                     index = cell_base + x
  148.                     old_id = old_decoder.block_id(index)
  149.                     if old_id not in AIR_IDS:
  150.                         new_id = new_decoder.block_id(index)
  151.                         if new_id in AIR_IDS:
  152.                             if target_substr and target_substr not in old_id:
  153.                                 continue
  154.                             yield x, world_y, z, old_id, new_id
  155.  
  156.  
  157. def parse_region_coords(path):
  158.     name = pathlib.Path(path).stem
  159.     match = re.fullmatch(r"r\.(-?\d+)\.(-?\d+)", name)
  160.     if not match:
  161.         return 0, 0
  162.     return int(match.group(1)), int(match.group(2))
  163.  
  164.  
  165. def main():
  166.     args = parse_args()
  167.     target = args.target_substring
  168.     if target:
  169.         target = target.lower()
  170.  
  171.     old_region_x, old_region_z = parse_region_coords(args.old_mca)
  172.     new_region_x, new_region_z = parse_region_coords(args.new_mca)
  173.  
  174.     if (old_region_x, old_region_z) != (new_region_x, new_region_z):
  175.         sys.stderr.write(
  176.             f"Warning: old and new region files have different region coordinates: "
  177.             f"old=({old_region_x},{old_region_z}) new=({new_region_x},{new_region_z})\n"
  178.         )
  179.  
  180.     old_region = anvil.Region.from_file(args.old_mca)
  181.     new_region = anvil.Region.from_file(args.new_mca)
  182.  
  183.     found = 0
  184.     for cx in range(32):
  185.         for cz in range(32):
  186.             try:
  187.                 old_chunk = old_region.get_chunk(cx, cz)
  188.                 new_chunk = new_region.get_chunk(cx, cz)
  189.             except Exception:
  190.                 continue
  191.  
  192.             for x, y, z, old_id, new_id in compare_chunks(old_chunk, new_chunk, target):
  193.                 wx = old_region_x * 512 + cx * 16 + x
  194.                 wz = old_region_z * 512 + cz * 16 + z
  195.                 print(f"{wx} {y} {wz}: {old_id} -> {new_id}")
  196.                 found += 1
  197.  
  198.     if found == 0:
  199.         print("No blocks found where old != air and new == air.")
  200.  
  201.  
  202. if __name__ == "__main__":
  203.     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
  204.     try:
  205.         main()
  206.     except BrokenPipeError:
  207.         sys.exit(0)
Advertisement
Add Comment
Please, Sign In to add comment