def version(self, version: Tuple[int, int, int]): if self._open: log.error( "Construction version cannot be changed after the object has been opened." ) return self._version = version
def platform(self, platform: str): if self._open: log.error( "Construction platform cannot be changed after the object has been opened." ) return self._platform = platform
def get_chunk_boxes( self, dimension: Dimension, selection: Union[SelectionGroup, SelectionBox, None] = None, create_missing_chunks=False, ) -> Generator[Tuple[Chunk, SelectionBox], None, None]: """ Given a selection will yield :class:`Chunk` and :class:`SelectionBox` instances into that chunk If not given a selection will use the bounds of the object. :param dimension: The dimension to take effect in. :param selection: SelectionGroup or SelectionBox into the level. If None will use :meth:`bounds` for the dimension. :param create_missing_chunks: If a chunk does not exist an empty one will be created (defaults to false). Use this with care. """ for (cx, cz), box in self.get_coord_box(dimension, selection, create_missing_chunks): try: chunk = self.get_chunk(cx, cz, dimension) except ChunkDoesNotExist: if create_missing_chunks: yield self.create_chunk(cx, cz, dimension), box except ChunkLoadError: log.error(f"Error loading chunk\n{traceback.format_exc()}") else: yield chunk, box
def selection(self, selection: SelectionGroup): if self._open: log.error( "Construction selection cannot be changed after the object has been opened." ) return self._selection = selection
def parse_export(plugin: dict, operations_dict: Dict[str, OperationLoader], path: str): try: operations_dict[path] = OperationLoader(plugin, path) except OperationLoadException as e: log.error(f"Error loading plugin {path}. {e}") except Exception as e: log.error(f"Exception loading plugin {path}. {e}")
def selection(self, selection: SelectionGroup): if self._open: log.error( "Construction selection cannot be changed after the object has been opened." ) return if selection.selection_boxes: self._selection = selection.selection_boxes[0] else: raise Exception("Given selection box is empty")
def value(self, value: Union[float, int, str]): if isinstance(value, float): self._value = value elif isinstance(value, int): self._value = float(value) elif isinstance(value, str) and value in states: self._value = states[value][1] else: log.error( f"Unrecognised chunk state {value}. Defaulting to fully generated.\nIf this is a new version report it to the developers." ) self._value = 2.0
def _load_obj(self, path: str) -> bool: if os.path.isdir(path): py_path = os.path.join(path, "__init__.py") if not os.path.isfile(py_path): return False import_path = ".".join( os.path.normpath( os.path.relpath( path, os.path.dirname(os.path.dirname( amulet.__file__)))).split(os.sep)) obj_name = os.path.basename(path) elif os.path.isfile(path): if not path.endswith(".py"): return False py_path = path import_path = ".".join( os.path.normpath( os.path.relpath( path[:-3], os.path.dirname(os.path.dirname( amulet.__file__)))).split(os.sep)) obj_name = os.path.basename(path[:-3]) else: return False with open(py_path) as f: first_line = f.readline() if first_line.strip() == f"# meta {self._object_type}": if obj_name in self._objects: log.error( f"Multiple {self._object_type} classes with the name {obj_name}" ) return False modu = importlib.import_module(import_path) if not hasattr(modu, "export"): log.error( f'{self._object_type} "{obj_name}" is missing the export attribute' ) return False c = getattr(modu, "export") if self._create_instance: self._objects[obj_name] = c() else: self._objects[obj_name] = c log.debug(f'Enabled {self._object_type} "{obj_name}"') return True return False
def _load_obj(self, module_name: str): modu = importlib.import_module(module_name) if hasattr(modu, "export"): c = getattr(modu, "export") if issubclass(c, self._base_class): if self._create_instance: self._objects[module_name] = c() else: self._objects[module_name] = c else: log.error( f"export for {module_name} must be a subclass of {self._base_class}" ) log.debug(f'Enabled {self._object_type} "{module_name}"')
def commit_chunk(self, chunk: "Chunk", *args): """ Save a universal format chunk to the Format database (not the disk database) call save method to write changed chunks back to the disk database :param chunk: The chunk object to translate and save :return: """ if not self.writeable: log.error("This object is not writeable") return try: self._commit_chunk(copy.deepcopy(chunk), *args) except Exception: log.error(f"Error saving chunk {chunk}", exc_info=True) self._changed = True
def get_moved_chunk_slice_box( self, dimension: Dimension, destination_origin: BlockCoordinates, selection: Optional[Union[SelectionGroup, SelectionBox]] = None, destination_sub_chunk_shape: Optional[int] = None, create_missing_chunks: bool = False, ) -> Generator[Tuple[Chunk, Tuple[ slice, slice, slice], SelectionBox, ChunkCoordinates, Tuple[ slice, slice, slice], SelectionBox, ], None, None, ]: """ Iterate over a selection and return slices into the source object and destination object given the origin of the destination. When copying a selection to a new area the slices will only be equal if the offset is a multiple of the chunk size. This will rarely be the case so the slices need to be split up into parts that intersect a chunk in the source and destination. :param dimension: The dimension to iterate over. :param destination_origin: The location where the minimum point of self.selection will end up :param selection: An optional selection. The overlap of this and self.selection will be used :param destination_sub_chunk_shape: the chunk shape of the destination object (defaults to self.sub_chunk_size) :param create_missing_chunks: Generate empty chunks if the chunk does not exist. :return: """ for ( (src_cx, src_cz), src_slices, src_box, (dst_cx, dst_cz), dst_slices, dst_box, ) in self.get_moved_coord_slice_box( dimension, destination_origin, selection, destination_sub_chunk_shape, create_missing_chunks, ): try: chunk = self.get_chunk(src_cx, src_cz, dimension) except ChunkDoesNotExist: chunk = self.create_chunk(dst_cx, dst_cz, dimension) except ChunkLoadError: log.error(f"Error loading chunk\n{traceback.format_exc()}") continue yield chunk, src_slices, src_box, (dst_cx, dst_cz), dst_slices, dst_box
def load_chunk(self, cx: int, cz: int, *args) -> "Chunk": """ Loads and creates a universal amulet.api.chunk.Chunk object from chunk coordinates. :param cx: The x coordinate of the chunk. :param cz: The z coordinate of the chunk. :return: The chunk at the given coordinates. """ if not self.readable: raise ChunkLoadError("This object is not readable") try: return self._load_chunk(cx, cz, *args) except ChunkDoesNotExist as e: raise e except Exception: log.error(f"Error loading chunk {cx} {cz}", exc_info=True) raise ChunkLoadError
def commit_chunk(self, chunk: "Chunk", dimension: Dimension): """ Save a universal format chunk to the Format database (not the disk database) call save method to write changed chunks back to the disk database :param chunk: The chunk object to translate and save :param dimension: The dimension to commit the chunk to. :return: """ try: self._verify_has_lock() except ObjectReadWriteError as e: log.error(e) try: self._commit_chunk(copy.deepcopy(chunk), dimension) except Exception: log.error(f"Error saving chunk {chunk}", exc_info=True) self._changed = True
def _safe_load( self, meth: Callable, args: Tuple[Any, ...], msg: str, load_error: Type[EntryLoadError], does_not_exist_error: Type[EntryDoesNotExist], ): try: self._verify_has_lock() except ObjectReadWriteError as e: raise does_not_exist_error(e) try: return meth(*args) except does_not_exist_error as e: raise e except Exception as e: log.error(msg.format(*args), exc_info=True) raise load_error(e) from e
def load_chunk(self, cx: int, cz: int, dimension: Dimension) -> "Chunk": """ Loads and creates a universal amulet.api.chunk.Chunk object from chunk coordinates. :param cx: The x coordinate of the chunk. :param cz: The z coordinate of the chunk. :param dimension: The dimension to load the chunk from. :return: The chunk at the given coordinates. :raises: ChunkLoadError or ChunkDoesNotExist as is relevant. """ try: self._verify_has_lock() except ObjectReadWriteError as e: raise ChunkLoadError(e) try: return self._load_chunk(cx, cz, dimension) except ChunkDoesNotExist as e: raise e except Exception as e: log.error(f"Error loading chunk {cx} {cz}", exc_info=True) raise ChunkLoadError(e)
def value(self, value: Union[float, int, str]): """ Set the status value. >>> chunk.status.value = 2 >>> chunk.status.value = "full" This can be a float/int or a string. If it is a string it is looked up in the states look up table to find the float value it corresponds with. :param value: The value to set as the generation stage. """ if isinstance(value, float): self._value = value elif isinstance(value, int): self._value = float(value) elif isinstance(value, str) and value in states: self._value = states[value][1] else: log.error( f"Unrecognised chunk state {value}. Defaulting to fully generated.\nIf this is a new version report it to the developers." ) self._value = 2.0
def _decode_biomes(self, chunk: Chunk, compound: TAG_Compound, floor_cy: int): biomes = compound.pop("Biomes", None) if isinstance(biomes, TAG_Int_Array): if (len(biomes) / 16) % 4: log.error( f"The biome array size must be 4x4x4xN but got an array of size {biomes.value.size}" ) else: arr = numpy.transpose( biomes.astype(numpy.uint32).reshape((-1, 4, 4)), (2, 0, 1), ) # YZX -> XYZ chunk.biomes = { sy + floor_cy: arr for sy, arr in enumerate( numpy.split( arr, arr.shape[1] // 4, 1, ) ) }
def _find(self): """Load all objects from the object directory""" directories = glob.iglob(os.path.join(glob.escape(self._directory), "**", ""), recursive=True) for d in directories: meta_path = os.path.join(d, f"{self._object_type}.meta") if not os.path.exists(meta_path): continue with open(meta_path) as fp: meta = json.load(fp) if meta["meta_version"] != self._supported_meta_version: log.error( f'Could not enable {self._object_type} located in "{d}" due to unsupported meta version' ) continue if (meta[self._object_type][f"{self._object_type}_version"] != self._supported_version): log.error( f'Could not enable {self._object_type} "{meta[self._object_type]["id"]}" due to unsupported {self._object_type} version' ) continue spec = importlib.util.spec_from_file_location( meta[self._object_type]["entry_point"], os.path.join(d, meta[self._object_type]["entry_point"] + ".py"), ) modu = importlib.util.module_from_spec(spec) spec.loader.exec_module(modu) if not hasattr(modu, f"{self._object_type.upper()}_CLASS"): log.error( f'{self._object_type} "{meta[self._object_type]["id"]}" is missing the {self._object_type.upper()}_CLASS attribute' ) continue c = getattr(modu, f"{self._object_type.upper()}_CLASS") if self._create_instance: self._loaded[meta[f"{self._object_type}"]["id"]] = c() else: self._loaded[meta[f"{self._object_type}"]["id"]] = c log.debug( f'Enabled {self._object_type} "{meta[self._object_type]["id"]}", version {meta[self._object_type]["wrapper_version"]}' ) self._is_loaded = True
def load_format(directory: str, _format: str = None, forced: bool = False) -> "WorldFormatWrapper": """ Loads the world located at the given directory with the appropriate format loader. :param directory: The directory of the world :param _format: The format name to use :param forced: Whether to force load the world even if incompatible :return: The loaded world """ if not os.path.isdir(directory): if os.path.exists(directory): log.error( f'The path "{directory}" does exist but it is not a directory') raise Exception( f'The path "{directory}" does exist but it is not a directory') else: log.error( f'The path "{directory}" is not a valid path on this file system.' ) raise Exception( f'The path "{directory}" is not a valid path on this file system.' ) if _format is not None: if _format not in formats.loader: log.error(f"Could not find _format loader {_format}") raise FormatLoaderInvalidFormat( f"Could not find _format loader {_format}") if not forced and not formats.loader.identify(directory) == _format: log.error(f"{_format} is incompatible") raise FormatLoaderMismatched(f"{_format} is incompatible") format_class = formats.loader.get_by_id(_format) else: format_class = formats.loader.get(directory) return format_class(directory)
def replace(world: "BaseLevel", dimension: Dimension, selection: SelectionGroup, options: dict): original_blocks = options.get("original_blocks", None) if not isinstance(original_blocks, list) and all( isinstance(block, Block) for block in original_blocks): log.error( "Replace operation was not given a list of source Block objects") return replacement_blocks = options.get("replacement_blocks", None) if not isinstance(replacement_blocks, list) and all( isinstance(block, Block) for block in replacement_blocks): log.error( "Replace operation was not given a list of destination Block objects" ) return original_blocks: List[Block] replacement_blocks: List[Block] if len(original_blocks) != len(replacement_blocks): if len(replacement_blocks) == 1: replacement_blocks = replacement_blocks * len(original_blocks) else: log.error( "Replace operation must be given the same number of destination blocks as source blocks" ) original_internal_ids = list( map(world.block_palette.get_add_block, original_blocks)) replacement_internal_ids = list( map(world.block_palette.get_add_block, replacement_blocks)) for chunk, slices, _ in world.get_chunk_slice_box(dimension, selection): old_blocks = chunk.blocks[slices] new_blocks = old_blocks.copy() for original_id, replacement_id in zip(original_internal_ids, replacement_internal_ids): new_blocks[old_blocks == original_id] = replacement_id chunk.blocks[slices] = new_blocks chunk.changed = True
def _error(name, msg): log.error(f"Error loading plugin {name}. {msg}")
def open_from(self, f: BinaryIO): f = BytesIO(f.read()) magic_num_1 = f.read(8) assert magic_num_1 == magic_num, f"This file is not a construction file." self._format_version = struct.unpack(">B", f.read(1))[0] if self._format_version == 0: f.seek(-magic_num_len, os.SEEK_END) magic_num_2 = f.read(8) assert ( magic_num_2 == magic_num ), "It looks like this file is corrupt. It probably wasn't saved properly" f.seek(-magic_num_len - INT_STRUCT.size, os.SEEK_END) metadata_end = f.tell() metadata_start = INT_STRUCT.unpack(f.read(INT_STRUCT.size))[0] f.seek(metadata_start) metadata = amulet_nbt.load( f.read(metadata_end - metadata_start), compressed=True, ) try: self._platform = metadata["export_version"]["edition"].value self._version = tuple( map(lambda v: v.value, metadata["export_version"]["version"]) ) except KeyError as e: raise KeyError(f'Missing export version identifying key "{e.args[0]}"') self._section_version = metadata["section_version"].value palette = unpack_palette(metadata["block_palette"]) selection_boxes = metadata["selection_boxes"].value.reshape(-1, 6).tolist() self._selection = SelectionGroup( [ SelectionBox((minx, miny, minz), (maxx, maxy, maxz)) for minx, miny, minz, maxx, maxy, maxz in selection_boxes ] ) self._populate_chunk_to_box() section_index_table = ( metadata["section_index_table"].value.view(SECTION_ENTRY_TYPE).tolist() ) if self._section_version == 0: for ( start_x, start_y, start_z, shape_x, shape_y, shape_z, position, length, ) in section_index_table: f.seek(position) nbt_obj = amulet_nbt.load(f.read(length)) if nbt_obj["blocks_array_type"].value == -1: blocks = None block_entities = None else: blocks = numpy.reshape( nbt_obj["blocks"].value, (shape_x, shape_y, shape_z) ) block_entities = parse_block_entities(nbt_obj["block_entities"]) start = numpy.array([start_x, start_y, start_z]) chunk_index: numpy.ndarray = start // self.sub_chunk_size shape = numpy.array([shape_x, shape_y, shape_z]) if numpy.any(shape <= 0): continue # skip sections with zero size if numpy.any( start + shape > (chunk_index + 1) * self.sub_chunk_size ): log.error( f"section in construction file did not fit in one sub-chunk. Start: {start}, Shape: {shape}" ) cx, cy, cz = chunk_index.tolist() self._chunk_to_section.setdefault((cx, cz), []).append( ConstructionSection( (start_x, start_y, start_z), (shape_x, shape_y, shape_z), blocks, palette, parse_entities(nbt_obj["entities"]), block_entities, ) ) else: raise Exception( f"This wrapper does not support any construction section version higher than {max_section_version}" ) else: raise Exception( f"This wrapper does not support any construction format version higher than {max_format_version}" )
def error(msg): log.error( f'Error loading plugin {os.path.basename(fpath)}. {msg}')
def _load_operations(path: str): if os.path.isdir(path): for fpath in glob.iglob(os.path.join(path, "*.py")): if fpath == __file__: continue mod = _load_module(fpath) if hasattr(mod, "export"): plugin = getattr(mod, "export") if not isinstance(plugin, dict): log.error(f'Error loading plugin {os.path.basename(fpath)}. Export must be a dictionary.') continue if not isinstance(plugin.get("name", None), str): log.error(f'Error loading plugin {os.path.basename(fpath)}. "name" in export must exist and be a string.') continue if not callable(plugin.get("operation", None)): log.error(f'Error loading plugin {os.path.basename(fpath)}. "operation" in export must exist and be a function.') continue inputs = plugin.get("inputs", []) if not isinstance(inputs, list): log.error(f'Error loading plugin {os.path.basename(fpath)}. "inputs" in export must be a list.') continue if "dst_box" in inputs or "dst_box_multiple" in inputs: if "structure_callable" in plugin: if not callable(plugin["structure_callable"]): log.error(f'Error loading plugin {os.path.basename(fpath)}. "structure_callable" must be a callable if defined.') continue elif "src_box" not in inputs: log.error(f'Error loading plugin {os.path.basename(fpath)}. "src_box" or "structure_callable" must be defined if "dst_box" or "dst_box_multiple" are.') continue elif "structure" in inputs: log.error(f'Error loading plugin {os.path.basename(fpath)}. "structure" cannot be defined if "dst_box" or "dst_box_multiple" are not.') continue elif "structure_callable" in plugin: log.error(f'Error loading plugin {os.path.basename(fpath)}. "structure_callable" cannot be defined if "dst_box" or "dst_box_multiple" are not.') continue if "options" in inputs and "wxoptions" in inputs: log.error(f'Error loading plugin {os.path.basename(fpath)}. Only one of "options" and "wxoptions" may be defined in "inputs" at once.') continue elif "options" in inputs and "options" not in plugin: log.error(f'Error loading plugin {os.path.basename(fpath)}. "options" was specificed in "inputs" but was not present in the dictionary.') continue elif "wxoptions" in inputs and "wxoptions" not in plugin: log.error(f'Error loading plugin {os.path.basename(fpath)}. "wxoptions" was specificed in "inputs" but was not present in the dictionary.') continue if not all(v in _input_options for v in inputs): for v in inputs: if v not in _input_options: log.error(f'Error loading plugin {os.path.basename(fpath)}. "{v}" is not a valid value in "inputs".') continue operations[fpath] = plugin