Last active
March 30, 2025 20:40
-
-
Save lowercasebtw/c107c2038d3d405cd441525b0555d26a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os, shutil, json | |
| from jproperties import Properties | |
| from os import path | |
| from json import JSONEncoder | |
| CONVERSION_SUFFIX = "_converted" | |
| MINECRAFT_PATH = path.join("assets", "minecraft") | |
| ATLASES_PATH = path.join(MINECRAFT_PATH, "atlases") | |
| TEXTURES_PATH = path.join(MINECRAFT_PATH, "textures") | |
| OPTIFINE_PATH = path.join(MINECRAFT_PATH, "optifine") | |
| ITEMS_PATH = path.join(MINECRAFT_PATH, "items") | |
| MODELS_PATH = path.join(MINECRAFT_PATH, "models") | |
| ITEM_MODELS_PATH = path.join(MODELS_PATH, "item") | |
| CIT_PATH = path.join(OPTIFINE_PATH, "cit") | |
| TEXTURES_CIT_SUFFIX = "cit_textures" | |
| class NamespacedKey(JSONEncoder): | |
| def __init__(self, namespace: str, path: str): | |
| self.namespace = namespace | |
| self.path = path | |
| @staticmethod | |
| def from_string(input: str): | |
| if len(input) == 0: | |
| raise Exception("Invalid namespaced identifier") | |
| parts = input.split(':') | |
| if len(parts) > 2: | |
| raise Exception("Invalid namespaced identifier") | |
| is_single = len(parts) == 1 | |
| return NamespacedKey("minecraft" if is_single else parts[0], parts[0] if is_single else parts[1]) | |
| def __eq__(self, other): | |
| if isinstance(other, NamespacedKey): | |
| return self.namespace == other.namespace and self.path == other.path | |
| return False | |
| def __hash__(self): | |
| return hash(self.namespace) + hash(self.path) | |
| def __str__(self): | |
| return f"{self.namespace}:{self.path}" | |
| def __repr__(self): | |
| return self.__str__() | |
| class Optional: | |
| def __init__(self, value): | |
| self.value = value | |
| self.has_value = not value is None | |
| @staticmethod | |
| def of_nullable(value): | |
| return Optional(value) | |
| @staticmethod | |
| def of(value): | |
| if value is None: | |
| raise Exception("Value is null") | |
| return Optional(value) | |
| @staticmethod | |
| def empty(): | |
| return Optional.of_nullable(None) | |
| class OptiFineCITItem: | |
| def __init__(self, name: str, item: NamespacedKey): | |
| self.name = name | |
| self.item = item | |
| self.enchantments = [] | |
| self.enchantment_level = 0 | |
| @staticmethod | |
| def parse(name: str, input: str): | |
| properties = Properties() | |
| properties.load(input) | |
| if not "items" in properties: | |
| print("Failed to parse OptiFineCITItem, missing \"items\" key") | |
| return Optional.empty() | |
| if not "type" in properties or not properties.get("type").data == "item": | |
| print("Failed to parse OptiFineCITItem, not a item") | |
| return Optional.empty() | |
| cit_item = OptiFineCITItem(name, NamespacedKey.from_string(properties.get("items").data)) | |
| if "enchantmentIDs" in properties: | |
| cit_item.with_enchantments(properties.get("enchantmentIDs").data) | |
| if "enchantmentLevels" in properties: | |
| cit_item.with_enchantment_level(properties.get("enchantmentLevels").data) | |
| return Optional.of(cit_item) | |
| # TODO: Support real syntax as this is probably totally wrong for anything else LOL | |
| def with_enchantments(self, enchantments: str): | |
| self.enchantments = map(lambda enchantment: NamespacedKey.from_string(enchantment), enchantments.replace(' ', '').split(',')) | |
| return self | |
| def with_enchantment_level(self, level: int): | |
| self.enchantment_level = int(level) | |
| return self | |
| def map_paths(root: os.PathLike, paths: list[str]): | |
| return map(lambda p: path.join(root, p), paths) | |
| def ensure_path(p: os.PathLike) -> os.PathLike: | |
| if not path.exists(p): | |
| os.makedirs(p) | |
| return p | |
| def generate_condition_branch(obj: dict[str, any], entry: dict[str, any]) -> dict[str, str | list | dict]: | |
| dict = { | |
| 'type': "minecraft:condition", | |
| 'property': "minecraft:component", | |
| 'predicate': "stored_enchantments" # TODO: Support both stored & normal | |
| } | |
| data = entry['data'] | |
| dict['value'] = [{'enchantments': list(map(lambda key: str(key), data['enchantments']))}] | |
| if "levels" in data: | |
| dict['value'][0]['levels'] = data['levels'] | |
| dict['on_true'] = { | |
| 'type': "minecraft:model", | |
| 'model': f"minecraft:item/cit/{entry['name']}" | |
| } | |
| dict['on_false'] = obj | |
| return dict | |
| def generate_condition(key: NamespacedKey, entries: list[dict[str, any]]) -> dict[str, str | dict]: | |
| dict = { 'type': "minecraft:model", 'model': f"{key.namespace}:item/{key.path}" } | |
| for entry in entries: | |
| dict = generate_condition_branch(dict, entry) | |
| return dict | |
| def generate_atlas(dst: os.PathLike): | |
| NEW_ATLASES_PATH = ensure_path(path.join(dst, ATLASES_PATH)) | |
| # TODO: Append to existing blocks.json if it exists | |
| with open(path.join(NEW_ATLASES_PATH, "blocks.json"), "w+") as file: | |
| file.write(json.dumps({ | |
| 'sources': [ | |
| { | |
| 'type': "directory", | |
| 'source': "cit_textures", | |
| 'prefix': "textures/cit_textures/" | |
| } | |
| ] | |
| }, indent=4)) | |
| file.close() | |
| def filter_items(cit_path: os.PathLike) -> tuple[list[OptiFineCITItem], bool]: | |
| failed = False | |
| cit_items = [] | |
| for properties_file in map_paths(cit_path, filter(lambda p: p.endswith(".properties"), os.listdir(cit_path))): | |
| with open(properties_file) as file: | |
| print(f" Converting {properties_file}") | |
| cit_optional = OptiFineCITItem.parse(os.path.splitext(os.path.basename(properties_file))[0], file.read()) | |
| if not cit_optional.has_value: | |
| failed = True | |
| file.close() | |
| break | |
| cit_items.append(cit_optional.value) | |
| file.close() | |
| os.remove(properties_file) | |
| return (cit_items, failed) | |
| def map_duplicate_items(items: list[object]): | |
| duplicates = {} | |
| for cit_item in items: | |
| obj = { | |
| 'name': cit_item.name, | |
| 'data': {'enchantments': cit_item.enchantments} | |
| } | |
| if cit_item.enchantment_level > 0: | |
| obj['data']['levels'] = { | |
| 'min': cit_item.enchantment_level, | |
| 'max': cit_item.enchantment_level | |
| } | |
| if cit_item.item in duplicates: | |
| dupes = duplicates.get(cit_item.item) | |
| dupes.append(obj) | |
| else: | |
| duplicates[cit_item.item] = [obj] | |
| return duplicates | |
| def cleanup(dst: os.PathLike, cit_path: os.PathLike): | |
| print("Cleaning up...") | |
| if len(os.listdir(cit_path)) == 0: | |
| os.rmdir(cit_path) | |
| NEW_OPTIFINE_PATH = path.join(dst, OPTIFINE_PATH) | |
| if len(os.listdir(NEW_OPTIFINE_PATH)) == 0: | |
| os.rmdir(NEW_OPTIFINE_PATH) | |
| def vanillaify(src: os.PathLike, dst: os.PathLike): | |
| THIS_CIT_PATH = path.join(src, CIT_PATH) | |
| if not path.exists(THIS_CIT_PATH) or not path.isdir(THIS_CIT_PATH): | |
| print(f"{src} did not contain any OptiFine CIT, ignorning...") | |
| return | |
| print(f"Converting {src} to vanilla CIT pack") | |
| shutil.copytree(src, dst) | |
| NEW_CIT_PATH = path.join(dst, CIT_PATH) | |
| NEW_ITEMS_PATH = ensure_path(path.join(dst, ITEMS_PATH)) | |
| NEW_TEXTURES_PATH = ensure_path(path.join(dst, TEXTURES_PATH)) | |
| PACK_TEXTURES_PATH = path.join(NEW_TEXTURES_PATH, TEXTURES_CIT_SUFFIX) | |
| os.makedirs(PACK_TEXTURES_PATH) | |
| for texture_file in map_paths(NEW_CIT_PATH, filter(lambda p: p.endswith(".png"), os.listdir(NEW_CIT_PATH))): | |
| shutil.move(texture_file, PACK_TEXTURES_PATH) | |
| generate_atlas(dst) | |
| (cit_items, failed) = filter_items(NEW_CIT_PATH) | |
| if failed: | |
| shutil.rmtree(dst) | |
| print("Failed!") | |
| return | |
| # Group into singletons | |
| CIT_ITEM_MODELS_PATH = ensure_path(path.join(dst, ITEM_MODELS_PATH, "cit")) | |
| duplicates = map_duplicate_items(cit_items) | |
| for key in duplicates.keys(): | |
| # Item Models | |
| entry = duplicates[key] | |
| for item in entry: | |
| item_name = item['name'] | |
| item_path = path.join(CIT_ITEM_MODELS_PATH, f"{item_name}.json") | |
| with open(item_path, "w+") as file: | |
| # TODO: Possibly get original model and swap layer0 texture or whatever | |
| file.write(json.dumps({ | |
| 'parent': "minecraft:item/generated", # TODO: Get real parent for item type | |
| 'textures': { | |
| 'layer0': f"minecraft:textures/{TEXTURES_CIT_SUFFIX}/{item_name}" | |
| } | |
| }, indent=4)) | |
| file.close() | |
| # Item Defenitions | |
| item_path = path.join(NEW_ITEMS_PATH, f"{key.path}.json") | |
| # exists = path.exists(item_path) | |
| with open(item_path, "w+") as file: | |
| defenition = {'model': generate_condition(key, entry)} | |
| # TODO | |
| # if exists: | |
| # original = json.loads(file.read()) | |
| # if "model" in original: | |
| # defenition.model['on_false'] = original['model'] | |
| # file.seek(0) | |
| file.write(json.dumps(defenition, indent=4)) | |
| file.close() | |
| # Cleanup | |
| cleanup(dst, NEW_CIT_PATH) | |
| print("Done!") | |
| def main(): | |
| folders = map_paths('.', filter(lambda key: path.isdir(key) and not key.endswith(CONVERSION_SUFFIX), os.listdir('.'))) | |
| for folder in folders: | |
| CONVERTED_PATH_NAME = f"{folder}{CONVERSION_SUFFIX}" | |
| if path.exists(CONVERTED_PATH_NAME): | |
| shutil.rmtree(CONVERTED_PATH_NAME) | |
| print(f"Found pre-existing conversion for {folder}, removing...") | |
| files = os.listdir(folder) | |
| if not "pack.mcmeta" in files: | |
| print(f"Skipping {folder} as it is not a minecraft resourcepack") | |
| return | |
| vanillaify(folder, CONVERTED_PATH_NAME) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment