#!/usr/bin/env python # -*- coding: utf-8 -*- """This script can automatically generate blockstate and block model files, as well as textures for the Better Leaves Lite resourcepack.""" # Depencency imports import argparse import json import os import zipfile import shutil import time import random from PIL import Image from distutils.dir_util import copy_tree # Local imports from download_helper import downloadFromModrinth minify = False # Utility functions def printGreen(out): print("\033[92m{}\033[00m".format(out)) def printCyan(out): print("\033[96m{}\033[00m" .format(out)) def printOverride(out): print(" -> {}".format(out)) def dumpJson(data, f): json.dump(data, f, separators=(',', ':')) if minify else json.dump(data, f, indent=4) class BlockStateData: def __init__(self, namespace, block_name, state): self.namespace = namespace self.block_name = block_name self.state = state def fromFile(leaf, root, infile): with open(os.path.join(root, infile), "r") as f: printOverride("Loading blockstate data from: "+f.name) return BlockStateData.fromJson(leaf, json.load(f).get("blockStateData")) def fromJson(leaf, data): return BlockStateData(data["block"].split(":")[0], data["block"].split(":")[1], data["state"]) if "block" in data else BlockStateData(leaf.getId().split(":")[0], leaf.getId().split(":")[1], data["state"]) class LeafBlock: def __init__(self, namespace, block_name, texture_name): self.namespace = namespace self.block_name = block_name self.texture_name = texture_name base_model = "leaves" has_carpet = False has_no_tint = False has_texture_override = False should_generate_item_model = False use_legacy_model = False texture_prefix = "" overlay_texture_id = "" block_id_override = None texture_id_override = None dynamictrees_namespace = None blockstate_data = None sprite_overrides = None def getId(self): if (self.block_id_override != None): return self.block_id_override return self.namespace+":"+self.block_name def getTextureId(self): if (self.texture_id_override != None): return self.texture_id_override return self.namespace+":block/"+self.texture_prefix+self.texture_name class CarpetBlock: def __init__(self, carpet_id, leaf): self.carpet_id = carpet_id self.leaf = leaf if (leaf.has_no_tint): self.base_model = "leaf_carpet_notint" base_model = "leaf_carpet" # This is where the magic happens def autoGen(jsonData, args): notint_overrides = jsonData["noTint"] block_texture_overrides = jsonData["blockTextures"] overlay_textures = jsonData["overlayTextures"] compileonly_textures = jsonData["compileOnly"] block_id_overrides = jsonData["blockIds"] leaves_with_carpet = jsonData["leavesWithCarpet"] dynamictrees_namespaces = jsonData["dynamicTreesNamespaces"] generate_itemmodels_overrides = jsonData["generateItemModels"] block_state_copies = jsonData["blockStateCopies"] print("Generating assets...") if (os.path.exists("./assets")): shutil.rmtree("./assets") copy_tree("./base/assets/", "./assets/") if minify: minifyJsonFiles() filecount = 0 if (args.programmer): unpackTexturepacks("./input/programmer_art") unpackTexturepacks() unpackMods() scanModsForTextures() for root, dirs, files in os.walk("./input/assets"): for infile in files: if infile.endswith(".png") and (len(root.split("/")) > 3): texture_name = infile.replace(".png", "") leaf = LeafBlock(root.split("/")[3], texture_name, texture_name) # Handle leaf textures in subfolders if (len(root.split("/")) > 6): leaf.texture_prefix = root.split("/")[6]+"/" if (leaf.block_name == "leaves"): # For mods that use a structure like "texture/woodtype/leaves.png" leaf.block_name = leaf.texture_prefix.replace("/", "_")+leaf.block_name printGreen(leaf.getId()) printOverride("Auto-redirected from "+leaf.getId()) else: # For mods that use a structure like "texture/natural/some_leaves.png" printGreen(leaf.getId()) printOverride("Prefix: "+ leaf.texture_prefix); else: printGreen(leaf.getId()) # We don't want to generate assets for overlay textures if (leaf.getTextureId()) in overlay_textures.values(): printOverride("Skipping overlay texture") continue # We don't want to generate assets for compile-only textures if (leaf.getTextureId()) in compileonly_textures: printOverride("Skipping compile-only texture") continue texture = Image.open(os.path.join(root, infile)) leaf.use_legacy_model = texture.size[0] != texture.size[1] if leaf.use_legacy_model: printOverride("Animated – using legacy model") if args.legacy: leaf.use_legacy_model = True printOverride("Using legacy model as requested") # Generate texture if not leaf.use_legacy_model: generateTexture(root, infile, args.programmer) # Set block id and apply overrides if leaf.getId() in block_id_overrides: leaf.block_id_override = block_id_overrides[leaf.getId()] printOverride("ID Override: "+leaf.getId()) # Set texture id and apply overrides leaf.has_texture_override = leaf.getId() in block_texture_overrides if leaf.has_texture_override: leaf.texture_id_override = block_texture_overrides[leaf.getId()] printOverride("Texture Override: "+leaf.getTextureId()) # Check if the block appears in the notint overrides leaf.has_no_tint = leaf.getId() in notint_overrides if leaf.use_legacy_model: leaf.base_model = "leaves_legacy" elif leaf.has_no_tint: leaf.base_model = "leaves_notint" printOverride("No tint") # Check if the block has an additional overlay texture if leaf.getId() in overlay_textures: leaf.base_model = "leaves_overlay" leaf.overlay_texture_id = overlay_textures[leaf.getId()] printOverride("Has overlay texture: "+leaf.overlay_texture_id) # Check if the block has a dynamic trees addon namespace if (leaf.namespace) in dynamictrees_namespaces: leaf.dynamictrees_namespace = dynamictrees_namespaces[leaf.namespace] # Check if the block should generate an item model if leaf.getId() in generate_itemmodels_overrides: leaf.should_generate_item_model = True printOverride("Also generating item model") # Check for blockstate data if infile.replace(".png", ".betterleaves.json") in files: with open(os.path.join(root, infile.replace(".png", ".betterleaves.json")), "r") as f: jsonFile = json.load(f) if "blockStateData" in jsonFile: leaf.blockstate_data = BlockStateData.fromFile(leaf, root, infile.replace(".png", ".betterleaves.json")) if "spriteOverrides" in jsonFile: leaf.sprite_overrides = jsonFile["spriteOverrides"] # Generate blockstates & models generateBlockstate(leaf, block_state_copies) generateBlockModels(leaf) generateItemModel(leaf) # Certain mods contain leaf carpets. # Because we change the leaf texture, we need to fix the carpet models. if (leaf.getId()) in leaves_with_carpet: carpet_ids = leaves_with_carpet[leaf.getId()] if not isinstance(carpet_ids, list): carpet_ids = [carpet_ids] # In case only one carpet is provided (as a string), turn it into a list for carpet_id in carpet_ids: carpet = CarpetBlock(carpet_id, leaf) generateCarpetAssets(carpet) printOverride(f"Generating leaf carpet: {carpet.carpet_id}") filecount += 1 # End of autoGen print() if (args.programmer): cleanupTexturepacks("./input/programmer_art") cleanupTexturepacks() cleanupMods() printCyan("Processed {} leaf blocks".format(filecount)) def unpackMods(): for root, dirs, files in os.walk("./input/mods"): for infile in files: if infile.endswith(".jar"): print("Unpacking mod: "+infile) zf = zipfile.ZipFile(os.path.join(root, infile), 'r') zf.extractall(os.path.join(root, infile.replace(".jar", "_temp"))) zf.close() def cleanupMods(): if (os.path.exists("./input/mods")): shutil.rmtree("./input/mods") os.makedirs("./input/mods") def scanModsForTextures(): for root, dirs, files in os.walk("./input/mods"): for infile in files: if len(root.split("assets")) > 1: assetpath = root.split("assets")[1][1:] modid = assetpath.split("textures")[0].replace("/", "") if "textures/block" in root and infile.endswith(".png") and "leaves" in infile: print(f"Found texture {assetpath}/{infile} in mod {modid}") inputfolder = os.path.join("./input/assets/", assetpath) os.makedirs(inputfolder, exist_ok=True) shutil.copyfile(os.path.join(root, infile), os.path.join(inputfolder, infile)) def unpackTexturepacks(rootFolder="./input/texturepacks"): for root, dirs, files in os.walk(rootFolder): for infile in files: if infile.endswith(".zip"): print("Unpacking texturepack: "+infile) zf = zipfile.ZipFile(os.path.join(root, infile), 'r') zf.extractall(os.path.join(root, infile.replace(".zip", "_temp"))) zf.close() def cleanupTexturepacks(rootFolder="./input/texturepacks"): for root, dirs, files in os.walk(rootFolder): for folder in dirs: if folder.endswith("_temp"): shutil.rmtree(os.path.join(root, folder)) def scanPacksForTexture(baseRoot, baseInfile, rootFolder="./input/texturepacks"): for root, dirs, files in os.walk(rootFolder): for infile in files: if "assets" in root and "assets" in baseRoot: if infile.endswith(".png") and (len(root.split("/")) > 3) and (baseInfile == infile) and (root.split("assets")[1] == baseRoot.split("assets")[1]): printCyan(" Using texture from: " + root.split("assets")[0].replace(rootFolder, "")) return root; return baseRoot def generateTexture(root, infile, useProgrammerArt=False): outfolder = root.replace("assets", "").replace("input", "assets") os.makedirs(outfolder, exist_ok=True) # Check for texture stitching data textureMap = {} if os.path.isfile(os.path.join(root, infile.replace(".png", ".betterleaves.json"))): with open(os.path.join(root, infile.replace(".png", ".betterleaves.json")), "r") as f: json_data = json.load(f) if "textureStitching" in json_data: printOverride("Using texture stitching data from: " + f.name) # Create texture map from stitching data for key, value in json_data["textureStitching"].items(): if "-" in key: for i in range(int(key.split("-")[0]), int(key.split("-")[1])+1): textureMap[str(i)] = value else: textureMap[key] = value # Turn texture map into absolute paths for key, value in textureMap.items(): textureRoot = f"./input/assets/{value.split(':')[0]}/textures/" textureFile = value.split(":")[1] + ".png" if "/" in textureFile: textureRoot += textureFile.rsplit("/")[0] textureFile = textureFile[len(textureFile.rsplit("/")[0])+1:] # The rest of the string, starting behind the first '/' textureRoot = scanPacksForTexture(textureRoot, textureFile) if useProgrammerArt: root = scanPacksForTexture(textureRoot, textureFile, "./input/programmer_art") textureMap[key] = os.path.join(textureRoot, textureFile) root = scanPacksForTexture(root, infile) if useProgrammerArt: root = scanPacksForTexture(root, infile, "./input/programmer_art") outfile = os.path.splitext(os.path.join(outfolder, infile))[0] + ".png" if infile != outfile: try: # First, let's open the regular texture vanilla = Image.open(os.path.join(root, infile)) width, height = vanilla.size # Second, let's generate a transparent texture that's twice the size transparent = Image.new("RGBA", [int(2 * s) for s in vanilla.size], (255, 255, 255, 0)) out = transparent.copy() # Now we paste the regular texture in a 3x3 grid, centered in the middle for x in range(-1, 2): for y in range(-1, 2): texture = vanilla index = (x + 2) + (y + 1) * 3 # Turns coordinates into a number from 1 to 9 if str(index) in textureMap: # Load texture from texture stitching map texture = Image.open(textureMap[str(index)]) out.paste(texture, (int(width / 2 + width * x), int(height / 2 + height * y))) # As the last step, we apply our custom mask to round the edges and smoothen things out mask_location = f"input/masks/{width}px" # If possible, use a mask designed for the texture's size if not os.path.isdir(mask_location) or len(os.listdir(mask_location)) == 0: mask_location = "input/masks/16px" random.seed(infile) # Use the filename as a seed. This ensures we always get the same mask per block. mask_location += f"/{random.choice(os.listdir(mask_location))}" # Choose a random mask to get some variation between the different types of leaves mask = Image.open(mask_location).convert('L').resize(out.size, resample=Image.NEAREST) out = Image.composite(out, transparent, mask) # Finally, we save the texture to the assets folder out.save(outfile, vanilla.format) except IOError: print("Error while generating texture for '%s'" % infile) def generateBlockstate(leaf, block_state_copies): mod_namespace = leaf.getId().split(":")[0] block_name = leaf.getId().split(":")[1] block_state_namespace = mod_namespace block_state_name = block_name state = "" if leaf.blockstate_data != None: # In case custom blockstate data is defined block_state_namespace = leaf.blockstate_data.namespace block_state_name = leaf.blockstate_data.block_name state = leaf.blockstate_data.state # Create structure for blockstate file block_state_file = f"assets/{block_state_namespace}/blockstates/{block_state_name}.json" block_state_data = { "variants": { f"{state}": [] } } if os.path.exists(block_state_file): # In case the blockstate file already exists, we want to add to it with open(block_state_file, "r") as f: block_state_data = json.load(f) if state not in block_state_data["variants"]: block_state_data["variants"][state] = [] # Add four rotations for each of the four individual leaf models for i in range(1, 5): block_state_data["variants"][state] += { "model": f"{mod_namespace}:block/{block_name}{i}" }, { "model": f"{mod_namespace}:block/{block_name}{i}", "y": 90 }, { "model": f"{mod_namespace}:block/{block_name}{i}", "y": 180 }, { "model": f"{mod_namespace}:block/{block_name}{i}", "y": 270 }, # Create blockstates folder if it doesn't exist already os.makedirs("assets/{}/blockstates/".format(block_state_namespace), exist_ok=True) # Write blockstate file with open(block_state_file, "w") as f: dumpJson(block_state_data, f) # Do the same for the dynamic trees namespace if leaf.dynamictrees_namespace != None: dyntrees_block_state_file = f"assets/{leaf.dynamictrees_namespace}/blockstates/{block_name}.json" os.makedirs("assets/{}/blockstates/".format(leaf.dynamictrees_namespace), exist_ok=True) # Write blockstate file with open(dyntrees_block_state_file, "w") as f: dumpJson(block_state_data, f) # Additional block state copies if (leaf.getId()) in block_state_copies: block_state_copy_ids = block_state_copies[leaf.getId()] if not isinstance(block_state_copy_ids, list): block_state_copy_ids = [block_state_copy_ids] # In case only one blockstate is provided (as a string), turn it into a list for block_state_copy_id in block_state_copy_ids: block_state_copy_namespace = block_state_copy_id.split(":")[0] block_state_copy_name = block_state_copy_id.split(":")[1] block_state_copy_file = f"assets/{block_state_copy_namespace}/blockstates/{block_state_copy_name}.json" os.makedirs("assets/{}/blockstates/".format(block_state_copy_namespace), exist_ok=True) # Write blockstate file with open(block_state_copy_file, "w") as f: dumpJson(block_state_data, f) printOverride(f"Writing blockstate copy: {block_state_copy_id}") def generateBlockModels(leaf): mod_namespace = leaf.getId().split(":")[0] block_name = leaf.getId().split(":")[1] # Create models folder if it doesn't exist already os.makedirs("assets/{}/models/block/".format(mod_namespace), exist_ok=True) # Create the four individual leaf models for i in range(1, 5): # Create structure for block model file block_model_file = f"assets/{mod_namespace}/models/block/{block_name}{i}.json" block_model_data = { "parent": f"betterleaves:block/{leaf.base_model}{i}", "textures": { "all": f"{leaf.getTextureId()}" } } # Add overlay texture on request if (leaf.overlay_texture_id != ""): block_model_data["textures"]["overlay"] = leaf.overlay_texture_id # Add additional textures if (leaf.sprite_overrides): for key in leaf.sprite_overrides: block_model_data["textures"][key] = leaf.sprite_overrides[key]; # Write block model file with open(block_model_file, "w") as f: dumpJson(block_model_data, f) def generateItemModel(leaf): mod_namespace = leaf.getId().split(":")[0] block_name = leaf.getId().split(":")[1] # Create models folder if it doesn't exist already os.makedirs("assets/{}/models/block/".format(mod_namespace), exist_ok=True) block_item_model_file = f"assets/{mod_namespace}/models/block/{block_name}.json" if leaf.has_texture_override: # Used for items that have a different texture than the block model item_model_data = { "parent": f"betterleaves:block/{leaf.base_model}", "textures": { "all": f"{mod_namespace}:block/{block_name}" } } else: # By default, the regular block texture is used item_model_data = { "parent": f"betterleaves:block/{leaf.base_model}", "textures": { "all": f"{leaf.getTextureId()}" } } # Add overlay texture on request if (leaf.overlay_texture_id != ""): item_model_data["textures"]["overlay"] = leaf.overlay_texture_id with open(block_item_model_file, "w") as f: dumpJson(item_model_data, f) if leaf.should_generate_item_model: # Create models folder if it doesn't exist already os.makedirs("assets/{}/models/item/".format(mod_namespace), exist_ok=True) item_model_file = f"assets/{mod_namespace}/models/item/{block_name}.json" with open(item_model_file, "w") as f: dumpJson(item_model_data, f) def generateCarpetAssets(carpet): mod_namespace = carpet.carpet_id.split(":")[0] block_name = carpet.carpet_id.split(":")[1] # Create blockstate folder if it doesn't exist already os.makedirs("assets/{}/blockstates/".format(mod_namespace), exist_ok=True) # Create structure for blockstate file block_state_file = f"assets/{mod_namespace}/blockstates/{block_name}.json" block_state_data = { "variants": { "": [] } } # Add four rotations for the carpet model block_state_data["variants"][""] += { "model": f"{mod_namespace}:block/{block_name}" }, { "model": f"{mod_namespace}:block/{block_name}", "y": 90 }, { "model": f"{mod_namespace}:block/{block_name}", "y": 180 }, { "model": f"{mod_namespace}:block/{block_name}", "y": 270 }, # Write blockstate file with open(block_state_file, "w") as f: dumpJson(block_state_data, f) # Create models folder if it doesn't exist already os.makedirs("assets/{}/models/block/".format(mod_namespace), exist_ok=True) # Create structure for block model file block_model_file = f"assets/{mod_namespace}/models/block/{block_name}.json" block_model_data = { "parent": f"betterleaves:block/{carpet.base_model}", "textures": { "wool": f"{carpet.leaf.getTextureId()}" } } # Save the carpet block model file with open(block_model_file, "w") as f: dumpJson(block_model_data, f) def minifyJsonFiles(rootDir="./assets"): for root, dirs, files in os.walk(rootDir): for infile in files: if infile.endswith(".json"): minifyExistingJson(root, infile) def minifyExistingJson(root, infile): with open(os.path.join(root, infile), "r") as rf: data = json.load(rf) with open(os.path.join(root, infile), "w") as wf: json.dump(data, wf, separators=(',', ':')) def writeMetadata(args): edition = args.edition if isinstance(edition, list): edition = " ".join(args.edition) with open("./input/pack.mcmeta") as infile, open("pack.mcmeta", "w") as outfile: for line in infile: line = line.replace("${version}", args.version).replace("${edition}", edition).replace("${year}", str(time.localtime().tm_year)) outfile.write(line) # See https://stackoverflow.com/a/1855118 def zipdir(path, ziph): # ziph is zipfile handle for root, dirs, files in os.walk(path): for file in files: ziph.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), os.path.join(path, '..'))) # Creates a compressed zip file def makeZip(filename, programmer_art=False): with zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf: zipdir('assets/', zipf) zipf.write('pack.mcmeta') zipf.write('pack_programmer_art.png', arcname='pack.png') if programmer_art else zipf.write('pack.png') zipf.write('LICENSE') zipf.write('README.md') # This is the main entry point, executed when the script is run if __name__ == '__main__': start_time = time.perf_counter() parser = argparse.ArgumentParser( description='This script can automatically generate files for the Better Leaves Lite resourcepack.', epilog='Feel free to ask for help at http://discord.midnightdust.eu/') parser.add_argument('version', type=str) parser.add_argument('edition', nargs="*", type=str, default="§cCustom Edition", help="Define your edition name") parser.add_argument('--legacy', '-l', action='store_true', help="Use legacy models (from 8.1) for all leaves") parser.add_argument('--programmer', '-p', action='store_true', help="Use programmer art textures") parser.add_argument('--minify', '-m', action='store_true', help="Minify all JSON output files") parser.add_argument('--download', '-d', help="Downloads the requested resourcepack beforehand") args = parser.parse_args() print(f"Arguments: {args}") print() print("Motschen's Better Leaves Lite") print("https://github.com/TeamMidnightDust/BetterLeavesLite") print() if args.minify: minify = True if args.download != None: downloadFromModrinth(args.download) # Loads overrides from the json file f = open('./input/overrides.json') data = json.load(f) f.close() autoGen(data, args); writeMetadata(args) print() print("Zipping it up...") makeZip(f"Better-Leaves-{args.version}.zip" if not args.programmer else f"Better-Leaves-(Programmer-Art)-{args.version}.zip", args.programmer); print("Done!") print("--- Finished in %s seconds ---" % (round((time.perf_counter() - start_time)*1000)/1000))