Advertisement
EvilSupahFly

resource_pack_manager.py

Apr 6th, 2025
332
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 19.08 KB | Source Code | 0 0
  1. import os, json, copy
  2. from typing import Union, Iterable, Iterator, Optional
  3. from PIL import Image
  4. import numpy, glob, itertools, logging
  5. import amulet_nbt
  6.  
  7. from minecraft_model_reader.api import Block
  8. from minecraft_model_reader.api.resource_pack import BaseResourcePackManager
  9. from minecraft_model_reader.api.resource_pack.java import JavaResourcePack
  10. from minecraft_model_reader.api.mesh.block.block_mesh import (BlockMesh, FACE_KEYS, Transparency,)
  11. from minecraft_model_reader.api.mesh.util import rotate_3d
  12. from minecraft_model_reader.api.mesh.block.cube import (cube_face_lut, uv_rotation_lut, tri_face,)
  13.  
  14. log = logging.getLogger(__name__)
  15.  
  16. UselessImageGroups = {"colormap", "effect", "environment", "font", "gui", "map", "mob_effect", "particle",}
  17.  
  18. class JavaResourcePackManager(BaseResourcePackManager[JavaResourcePack]):
  19.     """A class to load and handle the data from the packs.
  20.    Packs are given as a list with the later packs overwriting the earlier ones."""
  21.  
  22.     def __init__(self, resource_packs: Union[JavaResourcePack, Iterable[JavaResourcePack]], load: bool = True,) -> None:
  23.         super().__init__()
  24.         self._blockstate_files: dict[tuple[str, str], dict] = {}
  25.         self._textures: dict[tuple[str, str], str] = {}
  26.         self._texture_is_transparent: dict[str, tuple[float, bool]] = {}
  27.         self._model_files: dict[tuple[str, str], dict] = {}
  28.         if isinstance(resource_packs, Iterable):
  29.             self._packs = list(resource_packs)
  30.         elif isinstance(resource_packs, JavaResourcePack):
  31.             self._packs = [resource_packs]
  32.         else:
  33.             raise Exception(f"Invalid format {resource_packs}")
  34.         if load:
  35.             for _ in self.reload():
  36.                 pass
  37.  
  38.     def _unload(self) -> None:
  39.         """Clear all loaded resources."""
  40.         super()._unload()
  41.         self._blockstate_files.clear()
  42.         self._textures.clear()
  43.         self._texture_is_transparent.clear()
  44.         self._model_files.clear()
  45.  
  46.     def _load_iter(self) -> Iterator[float]:
  47.         blockstate_file_paths: dict[tuple[str, str], str] = {}
  48.         model_file_paths: dict[tuple[str, str], str] = {}
  49.         transparency_cache_path = os.path.join(os.environ["CACHE_DIR"], "resource_packs", "java", "transparency_cache.json")
  50.         self._load_transparency_cache(transparency_cache_path)
  51.         self._textures[("minecraft", "missing_no")] = self.missing_no
  52.         pack_count = len(self._packs)
  53.         for pack_index, pack in enumerate(self._packs):
  54.             # pack_format=2 textures/blocks, textures/items - case sensitive
  55.             # pack_format=3 textures/blocks, textures/items - lower case
  56.             # pack_format=4 textures/block, textures/item
  57.             # pack_format=5 model paths and texture paths are now optionally namespaced
  58.  
  59.             pack_progress = pack_index / pack_count
  60.             yield pack_progress
  61.  
  62.             if pack.valid_pack and pack.pack_format >= 2:
  63.                 image_paths = glob.glob(os.path.join(glob.escape(pack.root_dir),"assets","*","textures","**","*.png",),recursive=True,)
  64.                 image_count = len(image_paths)
  65.                 sub_progress = pack_progress
  66.                 for image_index, texture_path in enumerate(image_paths):
  67.                     _, namespace, _, *rel_path_list = os.path.normpath(os.path.relpath(texture_path, pack.root_dir)).split(os.sep)
  68.                     if rel_path_list[0] not in UselessImageGroups:
  69.                         rel_path = "/".join(rel_path_list)[:-4]
  70.                         self._textures[(namespace, rel_path)] = texture_path
  71.                         if (os.stat(texture_path).st_mtime != self._texture_is_transparent.get(texture_path, [0])[0]):
  72.                             im: Image.Image = Image.open(texture_path)
  73.                             if im.mode == "RGBA":
  74.                                 alpha = numpy.array(im.getchannel("A").getdata())
  75.                                 texture_is_transparent = bool(numpy.any(alpha != 255))
  76.                             else:
  77.                                 texture_is_transparent = False
  78.  
  79.                             self._texture_is_transparent[texture_path] = (os.stat(texture_path).st_mtime,texture_is_transparent,)
  80.                     yield sub_progress + image_index / (image_count * pack_count * 3)
  81.  
  82.                 blockstate_paths = glob.glob(os.path.join(glob.escape(pack.root_dir),"assets","*","blockstates","*.json",))
  83.                 blockstate_count = len(blockstate_paths)
  84.                 sub_progress = pack_progress + 1 / (pack_count * 3)
  85.                 for blockstate_index, blockstate_path in enumerate(blockstate_paths):
  86.                     _, namespace, _, blockstate_file = os.path.normpath(os.path.relpath(blockstate_path, pack.root_dir)).split(os.sep)
  87.                     blockstate_file_paths[(namespace, blockstate_file[:-5])] = (blockstate_path)
  88.                     yield sub_progress + (blockstate_index) / (blockstate_count * pack_count * 3)
  89.  
  90.                 model_paths = glob.glob(os.path.join(glob.escape(pack.root_dir),"assets","*","models","**","*.json",),recursive=True,)
  91.                 model_count = len(model_paths)
  92.                 sub_progress = pack_progress + 2 / (pack_count * 3)
  93.                 for model_index, model_path in enumerate(model_paths):
  94.                     _, namespace, _, *rel_path_list = os.path.normpath(os.path.relpath(model_path, pack.root_dir)).split(os.sep)
  95.                     rel_path = "/".join(rel_path_list)[:-5]
  96.                     model_file_paths[(namespace, rel_path.replace(os.sep, "/"))] = (model_path)
  97.                     yield sub_progress + (model_index) / (model_count * pack_count * 3)
  98.  
  99.         os.makedirs(os.path.dirname(transparency_cache_path), exist_ok=True)
  100.         with open(transparency_cache_path, "w") as f:
  101.             json.dump(self._texture_is_transparent, f)
  102.  
  103.         for key, path in blockstate_file_paths.items():
  104.             with open(path) as fi:
  105.                 try:
  106.                     self._blockstate_files[key] = json.load(fi)
  107.                 except json.JSONDecodeError:
  108.                     log.error(f"Failed to parse blockstate file {path}")
  109.  
  110.         for key, path in model_file_paths.items():
  111.             with open(path) as fi:
  112.                 try:
  113.                     self._model_files[key] = json.load(fi)
  114.                 except json.JSONDecodeError:
  115.                     log.error(f"Failed to parse model file file {path}")
  116.  
  117.     @property
  118.     def textures(self) -> tuple[str, ...]:
  119.         """Returns a tuple of all the texture paths in the resource pack."""
  120.         return tuple(self._textures.values())
  121.  
  122.     def get_texture_path(self, namespace: Optional[str], relative_path: str) -> str:
  123.         """Get the absolute texture path from the namespace and relative path pair"""
  124.         if namespace is None:
  125.             return self.missing_no
  126.         key = (namespace, relative_path)
  127.         if key in self._textures:
  128.             return self._textures[key]
  129.         else:
  130.             return self.missing_no
  131.  
  132.     @staticmethod
  133.     def parse_state_val(val: Union[str, bool]) -> list:
  134.         """Convert the json block state format into a consistent format."""
  135.         if isinstance(val, str):
  136.             return [amulet_nbt.TAG_String(v) for v in val.split("|")]
  137.         elif isinstance(val, bool):
  138.             return [amulet_nbt.TAG_String("true") if val else amulet_nbt.TAG_String("false")]
  139.         else:
  140.             raise Exception(f"Could not parse state val {val}")
  141.  
  142.     def _get_model(self, block: Block) -> BlockMesh:
  143.         """Find the model paths for a given block state and load them."""
  144.         if (block.namespace, block.base_name) in self._blockstate_files:
  145.             blockstate: dict = self._blockstate_files[(block.namespace, block.base_name)]
  146.             if "variants" in blockstate:
  147.                 for variant in blockstate["variants"]:
  148.                     if variant == "":
  149.                         try:
  150.                             return self._load_blockstate_model(blockstate["variants"][variant])
  151.                         except Exception as e:
  152.                             log.error(f"Failed to load block model {blockstate['variants'][variant]}\n{e}")
  153.                     else:
  154.                         properties_match = Block.properties_regex.finditer(f",{variant}")
  155.                         if all(block.properties.get(match.group("name"),amulet_nbt.TAG_String(match.group("value")),).py_data == match.group("value") for match in properties_match):
  156.                             try:
  157.                                 return self._load_blockstate_model(blockstate["variants"][variant])
  158.                             except Exception as e:
  159.                                 log.error(f"Failed to load block model {blockstate['variants'][variant]}\n{e}")
  160.  
  161.             elif "multipart" in blockstate:
  162.                 models = []
  163.  
  164.                 for case in blockstate["multipart"]:
  165.                     try:
  166.                         if "when" in case:
  167.                             if "OR" in case["when"]:
  168.                                 if not any(all(block.properties.get(prop, None) in self.parse_state_val(val) for prop, val in prop_match.items()) for prop_match in case["when"]["OR"]):
  169.                                     continue
  170.                             elif "AND" in case["when"]:
  171.                                 if not all( all(block.properties.get(prop, None) in self.parse_state_val(val) for prop, val in prop_match.items()) for prop_match in case["when"]["AND"]):
  172.                                     continue
  173.                             elif not all(block.properties.get(prop, None) in self.parse_state_val(val) for prop, val in case["when"].items()):
  174.                                 continue
  175.                         if "apply" in case:
  176.                             try:
  177.                                 models.append(self._load_blockstate_model(case["apply"]))
  178.                             except Exception as e:
  179.                                 log.error(f"Failed to load block model {case['apply']}\n{e}")
  180.                     except Exception as e:
  181.                         log.error(f"Failed to parse block state for {block}\n{e}")
  182.                 return BlockMesh.merge(models)
  183.         return self.missing_block
  184.  
  185.     def _load_blockstate_model(self, blockstate_value: Union[dict, list[dict]]) -> BlockMesh:
  186.         """Load the model(s) associated with a block state and apply rotations if needed."""
  187.         if isinstance(blockstate_value, list):
  188.             blockstate_value = blockstate_value[0]
  189.         if "model" not in blockstate_value:
  190.             return self.missing_block
  191.         model_path = blockstate_value["model"]
  192.         rotx = int(blockstate_value.get("x", 0) // 90)
  193.         roty = int(blockstate_value.get("y", 0) // 90)
  194.         uvlock = blockstate_value.get("uvlock", False)
  195.  
  196.         model = copy.deepcopy(self._load_block_model(model_path))
  197.  
  198.         # TODO: rotate model based on uv_lock
  199.         return model.rotate(rotx, roty)
  200.  
  201.     def _load_block_model(self, model_path: str) -> BlockMesh:
  202.         """Load the model file associated with the Block and convert to a BlockMesh."""
  203.         # recursively load model files into one dictionary
  204.         java_model = self._recursive_load_block_model(model_path)
  205.  
  206.         # set up some variables
  207.         texture_dict = {}
  208.         textures = []
  209.         texture_count = 0
  210.         vert_count = {side: 0 for side in FACE_KEYS}
  211.         verts_src: dict[Optional[str], list[numpy.ndarray]] = {side: [] for side in FACE_KEYS}
  212.         tverts_src: dict[Optional[str], list[numpy.ndarray]] = {side: [] for side in FACE_KEYS}
  213.         tint_verts_src: dict[Optional[str], list[float]] = {side: [] for side in FACE_KEYS}
  214.         faces_src: dict[Optional[str], list[numpy.ndarray]] = {side: [] for side in FACE_KEYS}
  215.  
  216.         texture_indexes_src: dict[Optional[str], list[int]] = {side: [] for side in FACE_KEYS}
  217.         transparent = Transparency.Partial
  218.  
  219.         if set(java_model.get("textures", {})).difference({"particle"}) and not java_model.get("elements"):
  220.             return self.missing_block
  221.  
  222.         for element in java_model.get("elements", {}):
  223.             # iterate through elements (one cube per element)
  224.             element_faces = element.get("faces", {})
  225.  
  226.             opaque_face_count = 0
  227.             if (transparent and "rotation" not in element and element.get("to", [16, 16, 16]) == [16, 16, 16] and element.get("from", [0, 0, 0]) == [0, 0, 0] and len(element_faces) >= 6):
  228.                 # if the block is not yet defined as a solid block
  229.                 # and this element is a full size element
  230.                 # check if the texture is opaque
  231.                 transparent = Transparency.FullTranslucent
  232.                 check_faces = True
  233.             else:
  234.                 check_faces = False
  235.  
  236.             # lower and upper box coordinates
  237.             corners = numpy.sort(numpy.array([element.get("to", [16, 16, 16]), element.get("from", [0, 0, 0])],float,) / 16, 0,)
  238.  
  239.             # vertex coordinates of the box
  240.             box_coordinates = numpy.array(list(itertools.product(corners[:, 0], corners[:, 1], corners[:, 2])))
  241.  
  242.             for face_dir in element_faces:
  243.                 if face_dir in cube_face_lut:
  244.                     # get the cull direction. If there is an opaque block in this direction then cull this face
  245.                     cull_dir = element_faces[face_dir].get("cullface", None)
  246.                     if cull_dir not in FACE_KEYS:
  247.                         cull_dir = None
  248.  
  249.                     # get the relative texture path for the texture used
  250.                     texture_relative_path = element_faces[face_dir].get("texture", None)
  251.                     while isinstance(texture_relative_path, str) and texture_relative_path.startswith("#"):
  252.                         texture_relative_path = java_model["textures"].get(texture_relative_path[1:], None)
  253.                     texture_path_list = texture_relative_path.split(":", 1)
  254.                     if len(texture_path_list) == 2:
  255.                         namespace, texture_relative_path = texture_path_list
  256.                     else:
  257.                         namespace = "minecraft"
  258.  
  259.                     texture_path = self.get_texture_path(namespace, texture_relative_path)
  260.  
  261.                     if check_faces:
  262.                         if self._texture_is_transparent[texture_path][1]:
  263.                             check_faces = False
  264.                         else:
  265.                             opaque_face_count += 1
  266.  
  267.                     # get the texture
  268.                     if texture_relative_path not in texture_dict:
  269.                         texture_dict[texture_relative_path] = texture_count
  270.                         textures.append(texture_path)
  271.                         texture_count += 1
  272.  
  273.                     # texture index for the face
  274.                     texture_index = texture_dict[texture_relative_path]
  275.  
  276.                     # get the uv values for each vertex
  277.                     # TODO: get the uv based on box location if not defined
  278.                     texture_uv = (numpy.array(element_faces[face_dir].get("uv", [0, 0, 16, 16]), float,) / 16)
  279.                     texture_rotation = element_faces[face_dir].get("rotation", 0)
  280.                     uv_slice = (uv_rotation_lut[2 * int(texture_rotation / 90) :] + uv_rotation_lut[: 2 * int(texture_rotation / 90)])
  281.  
  282.                     # merge the vertex coordinates and texture coordinates
  283.                     face_verts = box_coordinates[cube_face_lut[face_dir]]
  284.                     if "rotation" in element:
  285.                         rotation = element["rotation"]
  286.                         origin = [r / 16 for r in rotation.get("origin", [8, 8, 8])]
  287.                         angle = rotation.get("angle", 0)
  288.                         axis = rotation.get("axis", "x")
  289.                         angles = [0, 0, 0]
  290.                         if axis == "x":
  291.                             angles[0] = -angle
  292.                         elif axis == "y":
  293.                             angles[1] = -angle
  294.                         elif axis == "z":
  295.                             angles[2] = -angle
  296.                         face_verts = rotate_3d(face_verts, *angles, *origin)
  297.  
  298.                     verts_src[cull_dir].append(face_verts)  # vertex coordinates for this face
  299.  
  300.                     tverts_src[cull_dir].append(texture_uv[uv_slice].reshape((-1, 2)))  # texture vertices
  301.  
  302.                     if "tintindex" in element_faces[face_dir]:
  303.                         tint_verts_src[cull_dir] += [0,1,0,] * 4  # TODO: set this up for each supported block
  304.                     else:
  305.                         tint_verts_src[cull_dir] += [1, 1, 1] * 4
  306.  
  307.                     # merge the face indexes and texture index
  308.                     face_table = tri_face + vert_count[cull_dir]
  309.                     texture_indexes_src[cull_dir] += [texture_index, texture_index]
  310.  
  311.                     # faces stored under cull direction because this is the criteria to render them or not
  312.                     faces_src[cull_dir].append(face_table)
  313.  
  314.                     vert_count[cull_dir] += 4
  315.  
  316.             if opaque_face_count == 6:
  317.                 transparent = Transparency.FullOpaque
  318.  
  319.         verts: dict[Optional[str], numpy.ndarray] = {}
  320.         tverts: dict[Optional[str], numpy.ndarray] = {}
  321.         tint_verts: dict[Optional[str], numpy.ndarray] = {}
  322.         faces: dict[Optional[str], numpy.ndarray] = {}
  323.         texture_indexes: dict[Optional[str], numpy.ndarray] = {}
  324.  
  325.         for cull_dir in FACE_KEYS:
  326.             face_array = faces_src[cull_dir]
  327.             if len(face_array) > 0:
  328.                 faces[cull_dir] = numpy.concatenate(face_array, axis=None)
  329.                 tint_verts[cull_dir] = numpy.concatenate(tint_verts_src[cull_dir], axis=None)
  330.                 verts[cull_dir] = numpy.concatenate(verts_src[cull_dir], axis=None)
  331.                 tverts[cull_dir] = numpy.concatenate(tverts_src[cull_dir], axis=None)
  332.                 texture_indexes[cull_dir] = numpy.array(texture_indexes_src[cull_dir], dtype=numpy.uint32)
  333.  
  334.         return BlockMesh(3,verts,tverts,tint_verts,faces,texture_indexes,tuple(textures),transparent,)
  335.  
  336.     def _recursive_load_block_model(self, model_path: str) -> dict:
  337.         """Load a model json file and recursively load and merge the parent entries into one json file."""
  338.         model_path_list = model_path.split(":", 1)
  339.         if len(model_path_list) == 2:
  340.             namespace, model_path = model_path_list
  341.         else:
  342.             namespace = "minecraft"
  343.         if (namespace, model_path) in self._model_files:
  344.             model = self._model_files[(namespace, model_path)]
  345.  
  346.             if "parent" in model:
  347.                 parent_model = self._recursive_load_block_model(model["parent"])
  348.             else:
  349.                 parent_model = {}
  350.             if "textures" in model:
  351.                 if "textures" not in parent_model:
  352.                     parent_model["textures"] = {}
  353.                 for key, val in model["textures"].items():
  354.                     parent_model["textures"][key] = val
  355.             if "elements" in model:
  356.                 parent_model["elements"] = model["elements"]
  357.  
  358.             return parent_model
  359.  
  360.         return {}
  361.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement