MCL2ResourcePackConverter/convert-mc-resource-pack.py

391 lines
17 KiB
Python
Executable File

#!/usr/bin/python3
import shutil, zipfile, sys, getopt, os, json, platform, csv, posixpath, subprocess
from PIL import Image
pack_format=1
TEXTURE_SIZE = 64
#Turn this to true to have files inserted into the MCL2 mod directory to use as default textures
generating_for_game=False
#This is the path to the zipped source resource pack
input_zip=""
#This is the path to the ouput directory mcl2 pack
output_directory="CONVERTED_TEXTURE_PACK"
#This is the path to the temporary unzipped source resource pack
input_directory="UNZIPPED_TMP_FILE_ABCDEFGHJKILM"
#"Linux" is linux, "Windows" is windows, "Darwin" is MacOS/Apple
platform=platform.system()
#Image Magick command variables
magick_prefix = ""
CONVERT="convert"
COMPOSITE="composite"
MOGRIFY="mogrify"
DISPLAY="display"
ANIMATE="animate"
COMPARE="compare"
CONJURE="conjure"
IDENTIFY="identify"
IMPORT="import"
MONTAGE="montage"
STREAM="stream"
magick_version = 7
VERSION="1.0"
warning_count=0
LOG_FILE="converter.log"
WARN_FILE="converter.warn.log"
warnLogFile = ""
logFile = ""
# Set to true to enable logging of warnings
debug = False
if debug:
warnLogFile = open(WARN_FILE, 'w')
logFile = open(LOG_FILE, 'w')
def warn(msg):
global warning_count, debug
print("WARNING: " + msg)
warning_count += 1
if debug:
warnLogFile.write("WARNING: " + msg + "\n")
def log(msg):
if debug:
logFile.write(msg + "\n")
# This function is called to check if an image is animated. Returns True if it is animated, false if it is a static image
def is_animated(imagePath):
global TEXTURE_SIZE
im = Image.open(imagePath)
width, height = im.size
if width == height:
return False
elif height % width == 0: # Height is an even factor of width, so it's probably animated frames of an item/block, so make sure we crop it.
return True
else: # Height is not an even factor of the width, so it probably isn't animated and is a mob texture
return False
def crop_image(xs,ys,xl,yl,xt,yt,src,dst):
global magick_prefix, CONVERT, COMPOSITE, MOGRIFY, DISPLAY, ANIMATE, COMPARE, CONJURE, IDENTIFY, IMPORT, MONTAGE, STREAM
if magick_version == 7:
try:
subprocess.run([magick_prefix, CONVERT, src, "-crop", str(xl) + "x" + str(yl) + "+" + str(xs) + "+" + str(ys), str(dst)], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Crop of source file " + str(src) + " failed! Skipping...")
print(err)
return False
return True
# Called to colorize a greyscale texture
def colorize_greyscale(src, colorMap, selectedColormapPixel, textureSize, dst):
global magick_prefix, CONVERT, COMPOSITE, MOGRIFY, DISPLAY, ANIMATE, COMPARE, CONJURE, IDENTIFY, IMPORT, MONTAGE, STREAM
if magick_version == 7:
try:
subprocess.run([magick_prefix, CONVERT, colorMap, "-crop", "1x1+"+selectedColormapPixel, "-depth", "8", "-resize", str(textureSize) + "x" + str(textureSize), "tmpABCDEFGH1234567File.png"], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Convert of greyscale of source file " + str(src) + " failed! Skipping...")
print(err)
try:
subprocess.run([magick_prefix, COMPOSITE, "-compose", "Multiply", "tmpABCDEFGH1234567File.png", src, dst], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Composite of greyscale of source file " + str(src) + " failed! Skipping...")
print(err)
return False
return True
# Called to colorize a greyscale texture and preserve alpha
def colorize_alpha(src, colorMap, selectedColormapPixel, textureSize, dst):
global magick_prefix, CONVERT, COMPOSITE, MOGRIFY, DISPLAY, ANIMATE, COMPARE, CONJURE, IDENTIFY, IMPORT, MONTAGE, STREAM
if not colorize_greyscale(src, colorMap, selectedColormapPixel, textureSize, "tmpBCDEFGHI123456.png"):
return False
if magick_version == 7:
try:
subprocess.run([magick_prefix, COMPOSITE, "-compose", "Dst_In", src, "tmpBCDEFGHI123456.png", "-alpha", "Set", str(dst)], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Colorizing alpha of source file " + str(src) + " failed! Skipping...")
print(err)
return False
return True
#This function checks to ensure that Image Magick is installed, and if it is, detects if the programs are installed separately or as one
def check_magick():
global magick_prefix, CONVERT, COMPOSITE, MOGRIFY, DISPLAY, ANIMATE, COMPARE, CONJURE, IDENTIFY, IMPORT, MONTAGE, STREAM
if shutil.which("magick") == None:
if shutil.which("compose") == None:
return False
else:
magick_prefix = ""
log("No prefix for magick")
return True
else:
log("Using magick prefix for magick")
magick_prefix = "magick"
#CONVERT="magick convert"
#COMPOSITE="magick composite"
#MOGRIFY="magick mogrify"
#DISPLAY="magick display"
#ANIMATE="magick animate"
#COMPARE="magick compare"
#CONJURE="magick conjure"
#IDENTIFY="magick identify"
#IMPORT="magick import"
#MONTAGE="magick montage"
#STREAM="magick stream"
return True
#Prints usage text
def show_usage():
print("Texture Pack Converter")
print("Version: " + VERSION)
print("This script is designed to take a MC (1.14) resource pack and create a texture pack for use with MineClone2.")
print("DO NOT REDISTRIBUTE ANY RESULUTING TEXTUREPACKS! COPYRIGHT AND LICENSING RESTRICTIONS MOST LIKELY STILL APPLY SO DO NOT REDISTRIBUTE ANY RESULTING TEXTURE PACKS!")
print("")
print("Usage:")
print(" python3 convert-mc-resource-pack.py [OPTIONS] INPUT-RESOURCE-PACK.zip [OUTPUT-NAME]")
print("")
print(" INPUT-RESOURCE-PACK does not have to be in a .zip file. If it is unzipped already then that is ok. If it is zipped, this script will unzip it into this directory.")
print(" OUTPUT-NAME is optional, if no output name is given, the resulting MineClone2 texture pack will be written to OUTPUT-NAME, or set to the value of .pack.name in the pack.mcmeta file.")
print(" OPTIONS may be one of:")
print(" -h --help Display this usage text.")
print(" -v --version Display the version of this script and then exit.")
print(" -d --debug Enables debug mode and logging to log and warning files.")
#Prints version message
def show_version():
print("Cross Platform Minecraft Resource Pack Converter Version " + VERSION)
#Processes cmdln args, exits after printing usage or version text. Handles errors for too many or too few args.
def process_args():
global input_zip, output_directory, logFile, warnLogFile, debug
try:
opts, args = getopt.gnu_getopt(sys.argv[1:], "hvd", ["help", "version", "debug"])
except getopt.GetoptError as err:
print(err)
usage()
sys.exit(2)
for o, a in opts:
if o in ("-h", "--help"):
show_usage()
sys.exit(0)
elif o in ("-v", "--version"):
show_version()
sys.exit(0)
elif o in ("-d", "--debug"):
debug = True
warnLogFile = open(WARN_FILE, 'w')
logFile = open(LOG_FILE, 'w')
if len(args) < 1:
print("No input file specified! You must specify the path to the resource pack you want to convert! Stopping.")
sys.exit(-1)
else:
input_zip = args[0]
if len(args) > 1:
if len(args) == 2:
output_directory=args[1]
else:
print("Too many arguments specified! Please see --help for correct usage!")
sys.exit(-1)
else:
output_directory=os.path.join(os.path.dirname(input_zip), os.path.splitext(os.path.basename(input_zip))[0] + "_CONVERTED", "")
#Ensures that the input file is a ZIP, checks that it exists, and extracts it to the temporary input_directory
def extract_zip():
global input_zip, output_path, platform, input_directory
if not ".zip" == os.path.splitext(input_zip)[-1]:
print("Error! Selected input file is not a ZIP file!")
sys.exit(-1)
if not os.path.isfile(input_zip):
print("Error! Selected input file does not exist!")
sys.exit(-1)
print("Attempting to extract targeted ZIP file...")
input_directory = os.path.join(os.path.dirname(input_zip), os.path.splitext(os.path.basename(input_zip))[0] + "_UNZIPPED", "")
with zipfile.ZipFile(input_zip, 'r') as zip_ref:
zip_ref.extractall(input_directory)
#Ensures that the unzipped resource pack looks like a resource pack and extracts the version information from it
def check_structure():
global input_directory, platform, output_directory, pack_format
print("Ensuring extracted resource pack is sane...")
if not os.path.isdir(input_directory):
print("Extraction failed! Double check your file permissions!")
sys.exit(-1)
if not os.path.isdir(os.path.join(input_directory, "assets", "")):
print("Extracted ZIP does not contain an assets directory! Are you sure it's a resource pack?")
sys.exit(-1)
if os.path.isfile(os.path.join(input_directory, "pack.mcmeta")):
print("Found MCMETA file! Yay! Parsing information...")
log("Found MCMETA file")
with open(os.path.join(input_directory, "pack.mcmeta")) as mcmeta:
pack_info = json.load(mcmeta)
pack_format = pack_info["pack"]["pack_format"]
print("Resource pack is in pack format " + str(pack_format))
log("Resource pack is in pack format " + str(pack_format))
else:
warn("No MCMETA file found! Assuming old version 1 format...")
pack_format = 1
#Creates the output directory
def create_output_directory():
global output_directory, platform
if os.path.isdir(output_directory):
warn("Output directory " + output_directory + " already exists, overwriting!")
shutil.rmtree(output_directory)
os.mkdir(output_directory)
log("Created output directory")
#Ensures that we have access to the CSV reference files we use to rename files
def check_csv_files():
global pack_format
if not os.path.isdir("pack_formats"):
print("ERROR! FATAL! The pack_formats directory could not be found! This directory is critical for this program to work!")
sys.exit(-1)
if not os.path.isfile(os.path.join("pack_formats", str(pack_format) + ".csv")):
print("FATAL: The pack format CSV file in the pack_formats directory could not be found! Stopping!")
print("Pack format: " + str(pack_format))
sys.exit(-1)
#Deletes all temporary files and directories.
def clean_up():
global input_directory, output_directory
#shutil.rmtree(input_directory)
def crop_and_copy():
global input_directory, pack_format, output_directory, generating_for_game, TEXTURE_SIZE
with open(os.path.join("pack_formats", str(pack_format) + ".csv")) as formatCSV:
readCSV = csv.reader(formatCSV, delimiter=',')
for source_path, source_file, target_path, target_file, xs, ys, xl, yl, xt, yt, blacklisted in readCSV:
if blacklisted == "y":
log("Source file " + source_file + " was blacklisted, skipping...")
continue
if(generating_for_game):
destination = os.path.join(target_path[1:].replace("/",os.sep), target_file)
else:
destination = os.path.join(output_directory, target_file)
#Replace our posix file separators with whatever separators we are using in the os
#Also select [1:] because of the leading / that we have on the source_path
source = os.path.join(source_path[1:].replace("/",os.sep), source_file)
source = os.path.join(input_directory, source)
if not os.path.isfile(source):
warn("Source file " + str(source_file) + " not found! Skipping...")
continue
if xs == "":
#Don't have to crop, just copy and rename
#Ensure that item/block is not animated, if it is animated, crop it.
if is_animated(source):
warn("Source file " + source_file + " seems to be animated, cropping a single tile...")
crop_image(0,0,TEXTURE_SIZE,TEXTURE_SIZE,0,0,source,destination)
else:
shutil.copyfile(source, destination)
else:
#Use Image Magick to crop and then rename
log("Cropping source file " + source_file)
crop_image(xs,ys,xl,yl,xt,yt,source,destination)
def colorize_textures():
global input_directory, output_directory
with open(os.path.join("pack_formats", "colorize-" + str(pack_format) + ".csv")) as formatCSV:
readCSV = csv.reader(formatCSV, delimiter=',')
for source_path, source_file, colormap_file, colormap_point, target_path, target_file in readCSV:
colormap = os.path.join(input_directory, os.path.join("/assets/minecraft/textures/colormap/"[1:].replace("/",os.sep), colormap_file))
source = os.path.join(source_path[1:].replace("/",os.sep), source_file)
source = os.path.join(input_directory, source)
#determine where the resulting file will go
if(generating_for_game):
destination = os.path.join(target_path[1:].replace("/",os.sep), target_file)
else:
destination = os.path.join(output_directory, target_file)
log("Colorizing greyscale source file " + source_file)
# Check that our input files exist
if not os.path.isfile(colormap):
warn("Color map " + str(colormap_file) + " not found for source file " + source_file + "! Skipping...")
continue
if not os.path.isfile(colormap):
warn("Greyscale Source file " + source_file + " not found! Skipping...")
continue
colorize_alpha(source, colormap, colormap_point, TEXTURE_SIZE, destination)
def convert_block_break():
global input_directory, output_directory, magick_prefix, CONVERT, COMPOSITE, MOGRIFY, DISPLAY, ANIMATE, COMPARE, CONJURE, IDENTIFY, IMPORT, MONTAGE, STREAM
log("Converting block break animation...")
destination = os.path.join(output_directory, "crack_anylength.png")
if pack_format == 4:
base = "/assets/minecraft/textures/block/destroy_stage_"
else:
base = "/assets/minecraft/textures/blocks/destroy_stage_"
if not os.path.isfile(os.path.join(input_directory, base[1:].replace("/",os.sep)+"0.png")):
warn("No block breaking animation found, skipping...")
return False
else:
done = False
i=0
source = os.path.join(input_directory, base[1:].replace("/",os.sep)+str(i)+".png")
shutil.copy(source,destination)
i=1
while not done:
source = os.path.join(input_directory, base[1:].replace("/",os.sep)+str(i)+".png")
if not os.path.isfile(source):
done = True
break
if os.path.isfile("tmpABCD.png"):
os.remove("tmpABCD.png")
try:
subprocess.run([magick_prefix, MONTAGE, "-tile", "1x" + str(i), "-geometry", "+0+0", "-background", "none", str(source), str(destination), destination], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Montage for crackig animation failed! Skipping...")
print(err)
return False
try:
subprocess.run([magick_prefix, CONVERT, destination, "-alpha", "on", "-background", "none", "-channel", "A", "-evaluate", "Min", "50%", str(destination)], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Convert for crackig animation failed! Skipping...")
print(err)
return False
i+=1
try:
subprocess.run([magick_prefix, CONVERT, destination, "-rotate", "180", destination], check=True, shell=True)
except subprocess.CalledProcessError as err:
warn("Rotation for crackig animation failed! Skipping...")
print(err)
return False
return True
process_args()
check_magick()
if not generating_for_game:
create_output_directory()
#User can specify to input an already extracted ZIP file instead of extracting all over again
if not os.path.isdir(input_zip):
extract_zip()
else:
input_directory = input_zip
check_structure()
check_csv_files()
print("Copying files...")
crop_and_copy()
print("Colorizing grass and leaves...")
colorize_textures()
print("Converting block break animation...")
convert_block_break()
clean_up()
log("Conversion Finished.")
if debug:
warnLogFile.close()
logFile.close()
print("Finished with " + str(warning_count) + " warnings.")