Amulet-Team / Amulet-Core

A Python library for reading and writing the Minecraft save formats. See Amulet for the actual editor.
https://www.amuletmc.com/
220 stars 33 forks source link

Pasting rotated Java schematic 180 degrees in .mcstructure causes 1 block of air on X-edge #252

Open MSpaceDev opened 1 year ago

MSpaceDev commented 1 year ago

Describe the bug I have a script that takes in .schem files from Java, and converts them to Bedrock .mcstructure.

I have managed to get the code working for rotations [0, 90, 270], however 180 is not working. There is an air gap on the X-most edge, where my structure is cut off. It pastes air, meaning the blank structure size is correct. The Z-edge is fine. What could be causing the X-edge to be air?

To Reproduce Steps to reproduce the behavior:

  1. Create new Amulet python script
    
    import glob
    from typing import List
    from argparse import ArgumentParser
    import shutil
    import os
    import sys

import amulet from amulet.api.level import World from amulet.api.level import Structure from amulet.level.formats.mcstructure import MCStructureFormatWrapper from amulet.level.formats.schematic import SchematicFormatWrapper from amulet.api.selection import SelectionGroup from amulet.api.selection import SelectionBox

def load_schematics(schematic_files: List[str]) -> List[Structure]: schematics = [] for schematic_file in schematic_files: with open(schematic_file, "rb") as f: schematics.append(amulet.load_level(schematic_file)) return schematics

def process_schematics(schematic_files: List[str], schematics: List[Structure]): for i, schematic in enumerate(schematics):

Get bounds of schematic

    bounds = schematic.bounds("main")
    x_size = bounds.max[0] - bounds.min[0]
    y_size = bounds.max[1] - bounds.min[1]
    z_size = bounds.max[2] - bounds.min[2]

    # Get basename of structure
    basename = schematic_files[i].replace(schematic_dir + "\\", "", -1)
    basename = basename.replace("\\", "_", -1)
    basename = basename.replace(" ", "_", -1)
    basename = basename.replace(".schem", "", -1)

    # Rotations
    rotations = [0, 90, 180, 270]
    for rotation in rotations:
        mcstructure_dir = os.path.join(output_dir, "structure", f"{basename}_{rotation}.mcstructure")
        mcstructure_wrapper = MCStructureFormatWrapper(mcstructure_dir)

        # Create new mcstructure file with correct bounds
        if rotation == 90 or rotation == 270:
            mcstructure_wrapper.create_and_open(platform, version, SelectionGroup(SelectionBox((0, 0, 0), (z_size, y_size, x_size))), True)
        else:
            mcstructure_wrapper.create_and_open(platform, version, SelectionGroup(SelectionBox((0, 0, 0), (x_size, y_size, z_size))), True)
        mcstructure_wrapper.save()
        mcstructure_wrapper.close()

        # Paste the schematic in the mcstructure
        if rotation == 90 or rotation == 270:
            paste_pos = (z_size / 2, y_size / 2, x_size / 2)
        else:
            paste_pos = (x_size / 2, y_size / 2, z_size / 2)

        mcstructure_level = amulet.load_level(mcstructure_dir)
        mcstructure_level.paste(schematic, "main", bounds, "main", paste_pos, (1.0, 1.0, 1.0), (0.0, rotation, 0.0))
        mcstructure_level.save()
        mcstructure_level.close()

def main():

Load all schematics

schematic_files = glob.glob(os.path.join("./schematics", "**/*.schem"))
schematics = load_schematics(schematic_files)

# Place schematics & create mcstructures
process_schematics(schematic_files, schematics)

2. Place `.schem` in `./schematics/category/file.schem`
3. Run python script and copy new `.mcstructures` in `./output`
4. Notice that rotations 0, 90 & 270 are correct.
5. Notice that rotation 180 is missing a single edge on the X-axis

**Expected behavior**
The generated `.mcstructure` for rotation 180 includes the full structure, and does not have the single axis air gap towards the X direction.

**Screenshots**
<img width="954" alt="ApplicationFrameHost_cK28dpqdGx" src="https://github.com/Amulet-Team/Amulet-Core/assets/21283721/4b8a4234-6cb0-4f0d-b2b6-91fb6536a681">

**Desktop (please complete the following information):**
 - OS: Windows 11
 - Program Version: Latest Amulet-Core v1.9.17

**Additional context**
Download `.schem`: https://drive.google.com/file/d/1yStCN8vAfiQXQm7wfSJiHESWKcxiuFPZ/view?usp=sharing
gentlegiantJGC commented 8 months ago

This is a problem stemming from rotating a structure with a mixture of even and odd lengths. If all sides are even the centre point is on the corner of the blocks and if both sides are odd it is in the centre of a block. If one is odd and one is even the rotation point is on the face between two of the blocks. When rotating this state the blocks don't align with the grid and it gets complicated. My solution to this problem was to round the rotation point down to the most negative corner of the centre block. Doing so has introduced this problem.

This code should solve your problem.

import glob
import os
from math import floor

import amulet
from amulet.level.formats.mcstructure import MCStructureFormatWrapper
from amulet.api.selection import SelectionGroup
from amulet.api.selection import SelectionBox

def process_schematics(schematic_dir: str, output_dir: str):
    for schematic_path in glob.glob(os.path.join(glob.escape(schematic_dir), "**/*.schem"), recursive=True):
        schematic = amulet.load_level(schematic_path)
        # Get bounds of schematic
        bounds = schematic.bounds("main")
        x_size = bounds.max_x - bounds.min_x
        y_size = bounds.max_y - bounds.min_y
        z_size = bounds.max_z - bounds.min_z

        # Get basename of structure
        basename = os.path.basename(schematic_path)[:-len(".schem")].replace(" ", "_")

        x_mid = x_size // 2
        y_mid = y_size // 2
        z_mid = z_size // 2

        for rotation, shape, paste_pos in (
            (0, (x_size, y_size, z_size), (x_mid, y_mid, z_mid)),
            (90, (z_size, y_size, x_size), (z_mid, floor(y_mid), x_size - x_mid)),
            (180, (x_size, y_size, z_size), (x_size - x_mid, floor(y_mid), z_size - z_mid)),
            (270, (z_size, y_size, x_size), (z_size - z_mid, y_mid, x_mid)),
        ):
            mcstructure_path = os.path.join(output_dir, f"{basename}_{rotation}.mcstructure")
            mcstructure_wrapper = MCStructureFormatWrapper(mcstructure_path)

            # Create new mcstructure file with correct bounds
            mcstructure_wrapper.create_and_open(
                "bedrock", (1, 20, 0),
                SelectionGroup(SelectionBox((0, 0, 0), shape)),
                True
            )
            mcstructure_wrapper.save()
            mcstructure_wrapper.close()

            # Paste the schematic in the mcstructure
            mcstructure_level = amulet.load_level(mcstructure_path)
            mcstructure_level.paste(schematic, "main", bounds, "main", paste_pos, (1.0, 1.0, 1.0), (0.0, rotation, 0.0))
            mcstructure_level.save()
            mcstructure_level.close()

def main():
    # Place schematics & create mcstructures
    process_schematics("schematics", "out")

if __name__ == '__main__':
    main()