9
0
mirror of https://github.com/Kas-tle/java2bedrock.sh.git synced 2025-12-29 11:29:07 +00:00
Files
J2B-Kas-tle/converter.sh
2022-10-22 07:38:07 +00:00

1075 lines
50 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
: ${1?'Please specify an input resource pack in the same directory as the script (e.g. ./converter.sh MyResourcePack.zip)'}
# define color placeholders
C_RED='\e[31m'
C_GREEN='\e[32m'
C_YELLOW='\e[33m'
C_BLUE='\e[36m'
C_GRAY='\e[37m'
C_CLOSE='\e[m'
# status message function
status_message () {
case $1 in
"completion")
printf "${C_GREEN}[+] ${C_GRAY}${2}${C_CLOSE}\n"
;;
"process")
printf "${C_YELLOW}[•] ${C_GRAY}${2}${C_CLOSE}\n"
;;
"critical")
printf "${C_RED}[X] ${C_GRAY}${2}${C_CLOSE}\n"
;;
"error")
printf "${C_RED}[ERROR] ${C_GRAY}${2}${C_CLOSE}\n"
;;
"info")
printf "${C_BLUE}${2}${C_CLOSE}\n"
;;
"plain")
printf "${C_GRAY}${2}${C_CLOSE}\n"
;;
esac
}
dependency_check () {
if command ${3} 2>/dev/null | grep -q "${4}"; then
status_message completion "Dependency ${1} satisfied"
else
status_message error "Dependency ${1} must be installed to proceed\nSee ${2}\nExiting script..."
exit 1
fi
}
user_input () {
if [[ -z "${!1}" ]]; then
status_message plain "${2} ${C_YELLOW}[${3}]\n"
read -p "${4}: " ${1}
echo
fi
}
wait_for_jobs () {
while test $(jobs -p | wc -w) -ge "$((2*$(nproc)))"; do wait -n; done
}
# ensure input pack exists
if ! test -f "${1}"; then
status_message error "Input resource pack ${1} is not in this directory"
exit 1
else
status_message process "Input file ${1} detected"
fi
# get flags
while getopts w:m:a:b:f:v: flag "${@:2}"
do
case "${flag}" in
w) warn=${OPTARG};;
m) merge_input=${OPTARG};;
a) attachable_material=${OPTARG};;
b) block_material=${OPTARG};;
f) fallback_pack=${OPTARG};;
v) default_asset_version=${OPTARG};;
esac
done
printf '\e[1;31m%-6s\e[m\n' "
███████████████████████████████████████████████████████████████████████████████
████████████████████████ # <!> # W A R N I N G # <!> # ████████████████████████
███████████████████████████████████████████████████████████████████████████████
███ This script has been provided as is. If your resource pack does not ███
███ entirely conform the vanilla resource specification, including but not ███
███ limited to, missing textures, improper parenting, improperly defined ███
███ predicates, and malformed JSON files, among other problems, there is a ███
███ strong possibility this script will fail. Please remedy any potential ███
███ resource pack formatting errors before attempting to make use of this ███
███ converter. You have been warned. ███
███████████████████████████████████████████████████████████████████████████████
███████████████████████████████████████████████████████████████████████████████
███████████████████████████████████████████████████████████████████████████████
"
if [[ ${warn} != "false" ]]; then
read -p $'\e[37mTo acknowledge and continue, press enter. To exit, press Ctrl+C.:\e[0m
'
fi
# ensure we have all the required dependencies
dependency_check "jq-1.6" "https://stedolan.github.io/jq/download/" "jq --version" "1.6"
dependency_check "sponge" "https://joeyh.name/code/moreutils/" "-v sponge" ""
dependency_check "imagemagick" "https://imagemagick.org/script/download.php" "convert --version" ""
dependency_check "spritesheet-js" "https://www.npmjs.com/package/spritesheet-js" "-v spritesheet-js" ""
status_message completion "All dependencies have been satisfied\n"
# initial configuration
status_message info "This script will now ask some configuration questions. Default values are yellow. Simply press enter to use the defaults.\n"
user_input merge_input "Is there an existing bedrock pack in this directory with which you would like the output merged? (e.g. input.mcpack)" "null" "Input pack to merge"
user_input attachable_material "What material should we use for the attachables?" "entity_alphatest_one_sided" "Attachable material"
user_input block_material "What material should we use for the blocks?" "alpha_test" "Block material"
user_input fallback_pack "From what URL should we download the fallback resource pack? (must be a direct link)\n Use 'none' if default resources are not needed." "null" "Fallback pack URL"
status_message plain "
Generating Bedrock 3D resource pack with settings:
${C_GRAY}Input pack to merge: ${C_BLUE}${merge_input:=null}
${C_GRAY}Attachable material: ${C_BLUE}${attachable_material:=entity_alphatest_one_sided}
${C_GRAY}Block material: ${C_BLUE}${block_material:=alpha_test}
${C_GRAY}Fallback pack URL: ${C_BLUE}${fallback_pack:=null}
"
# decompress our input pack
status_message process "Decompressing input pack"
unzip -n -q "${1}"
status_message completion "Input pack decompressed"
if [ ! -f pack.mcmeta ]
then
status_message error "Invalid resource pack! The pack.mcmeta file does not exist. Is the resource pack improperly compressed in an enclosing folder?"
exit 1
fi
# setup our initial config
status_message process "Iterating through all vanilla associated model JSONs to generate initial predicate config\nOn a large pack, this may take some time...\n"
if test -d "./assets/minecraft/models/item"; then confarg1="./assets/minecraft/models/item/*.json"; fi
if test -d "./assets/minecraft/models/block"; then confarg2="./assets/minecraft/models/block/*.json"; fi
# Download geyser mappings
status_message process "Downloading the latest geyser item mappings"
printf "\e[3m\e[37m"
echo
COLUMNS=$COLUMNS-1 curl --no-styled-output -#L -o item_mappings.json https://raw.githubusercontent.com/GeyserMC/mappings/master/items.json
echo
COLUMNS=$COLUMNS-1 curl --no-styled-output -#L -o item_texture.json https://raw.githubusercontent.com/Kas-tle/java2bedrockMappings/main/item_texture.json
echo
printf "${C_CLOSE}"
jq --slurpfile item_texture item_texture.json --slurpfile item_mappings item_mappings.json -n '[inputs | {(input_filename | sub("(.+)/(?<itemname>.*?).json"; .itemname)): .overrides?[]?}] |
def maxdur($input):
($item_mappings[] |
[to_entries | map(.key as $key | .value | .java_identifer = $key) | .[] | select(.max_damage)]
| map({(.java_identifer | split(":") | .[1]): (.max_damage)})
| add
| .[$input] // 1)
;
def bedrocktexture($input):
($item_texture[] | .[$input] // {"icon": "camera", "frame": 0})
;
def namespace:
if contains(":") then sub("\\:(.+)"; "") else "minecraft" end
;
[.[] | to_entries | map( select((.value.predicate.damage != null) or (.value.predicate.damaged != null) or (.value.predicate.custom_model_data != null)) |
(if .value.predicate.damage then (.value.predicate.damage * maxdur(.key) | round) else null end) as $damage
| (if .value.predicate.damaged == 0 then true else null end) as $unbreakable
| (if .value.predicate.custom_model_data then .value.predicate.custom_model_data else null end) as $custom_model_data |
{
"item": .key,
"bedrock_icon": bedrocktexture(.key),
"nbt": ({
"Damage": $damage,
"Unbreakable": $unbreakable,
"CustomModelData": $custom_model_data
}),
"path": ("./assets/" + (.value.model | namespace) + "/models/" + (.value.model | sub("(.*?)\\:"; "")) + ".json"),
"namespace": (.value.model | namespace),
"model_path": ((.value.model | sub("(.*?)\\:"; "")) | split("/")[:-1] | map(. + "/") | add[:-1]),
"model_name": ((.value.model | sub("(.*?)\\:"; "")) | split("/")[-1]),
"generated": "false"
}) | .[]]
| walk(if type == "object" then with_entries(select(.value != null)) else . end)
| to_entries | map( ((.value.geyserID = "gmdl_\(1+.key)" | .value.geometry = ("geometry.geyser_custom." + "gmdl_\(1+.key)")) | .value))
| INDEX(.geyserID)
' ${confarg1} ${confarg2} | sponge config.json
status_message completion "Initial predicate config generated"
# get a bash array of all model json files in our resource pack
status_message process "Generating an array of all model JSON files to crosscheck with our predicate config"
json_dir=($(find ./assets/**/models -type f -name '*.json'))
# ensure all our reference files in config.json exist, and delete the entry if they do not
status_message critical "Removing config entries that do not have an associated JSON file in the pack"
jq '
def real_file($input):
($ARGS.positional | index($input) // null);
map_values(if real_file(.path) != null then . else empty end)
' config.json --args ${json_dir[@]} | sponge config.json
# get a bash array of all our input models
status_message process "Creating a bash array for remaing models in our predicate config"
model_array=($(jq -r '[.[].path] | unique | .[]' config.json))
# find initial parental information
status_message process "Doing an initial sweep for level 1 parentals"
jq -n '
[def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end;
inputs | {
"path": (input_filename),
"parent": ("./assets/" + (.parent | namespace) + "/models/" + ((.parent? // empty) | sub("(.*?)\\:"; "")) + ".json")
}
]
' ${model_array[@]} | sponge parents.json
# add initial parental information to config.json
status_message critical "Removing config entries with non-supported parentals\n"
jq -s '
. as $global |
def intest($input_i): ($global | .[0] | map({(.path): .parent}) | add | .[$input_i]? // null);
def gtest($input_g):
[
"./assets/minecraft/models/block/block.json",
"./assets/minecraft/models/block/cube.json",
"./assets/minecraft/models/block/cube_column.json",
"./assets/minecraft/models/block/cube_directional.json",
"./assets/minecraft/models/block/cube_mirrored.json",
"./assets/minecraft/models/block/observer.json",
"./assets/minecraft/models/block/orientable_with_bottom.json",
"./assets/minecraft/models/block/piston_extended.json",
"./assets/minecraft/models/block/redstone_dust_side.json",
"./assets/minecraft/models/block/redstone_dust_side_alt.json",
"./assets/minecraft/models/block/template_single_face.json",
"./assets/minecraft/models/block/thin_block.json",
"./assets/minecraft/models/builtin/entity.json"
]
| index($input_g) // null;
.[1] | map_values(. + ({"parent": (intest(.path) // null)} | if gtest(.parent) == null then . else empty end))
| walk(if type == "object" then with_entries(select(.value != null)) else . end)
' parents.json config.json | sponge config.json
# obtain hashes of all model file paths to ensure consistent model naming
jq -r '.[] | [.geyserID, (.item + "_c" + (.nbt.CustomModelData | tostring) + "_d" + (.nbt.Damage | tostring) + "_u" + (.nbt.Unbreakable | tostring))] | @tsv | gsub("\\t";",")' config.json > paths.csv
function write_hash () {
local hash=$(echo -n "${1}" | md5sum | head -c 7) && echo "${2},${hash}" >> "${3}"
}
while IFS=, read -r gid predicate
do write_hash "${predicate}" "${gid}" "hashes.csv" &
done < paths.csv > /dev/null
jq -cR 'split(",")' hashes.csv | jq -s 'map({(.[0]): .[1]}) | add' > hashmap.json
jq --slurpfile hashmap hashmap.json '
map_values(
.geyserID as $gid
| . += {"path_hash": ("gmdl_" + ($hashmap[] | .[($gid)]))}
)
' config.json | sponge config.json
# create our initial directories for bp & rp
status_message process "Generating initial directory strucutre for our bedrock packs"
mkdir -p ./target/rp/models/blocks/geyser_custom && mkdir -p ./target/rp/textures/geyser/geyser_custom && mkdir -p ./target/rp/attachables/geyser_custom && mkdir -p ./target/rp/animations/geyser_custom && mkdir -p ./target/bp/blocks/geyser_custom && mkdir -p ./target/bp/items/geyser_custom
# copy over our pack.png if we have one
if test -f "./pack.png"; then
cp ./pack.png ./target/rp/pack_icon.png && cp ./pack.png ./target/bp/pack_icon.png
fi
# generate uuids for our manifests
uuid1=($(uuidgen))
uuid2=($(uuidgen))
uuid3=($(uuidgen))
uuid4=($(uuidgen))
# get pack description if we have one
pack_desc=($(jq -r '(.pack.description // "Geyser 3D Items Resource Pack")' ./pack.mcmeta))
# generate rp manifest.json
status_message process "Generating resource pack manifest"
jq -c --arg pack_desc "${pack_desc}" --arg uuid1 "${uuid1}" --arg uuid2 "${uuid2}" -n '
{
"format_version": 2,
"header": {
"description": "Adds 3D items for use with a Geyser proxy",
"name": $pack_desc,
"uuid": ($uuid1 | ascii_downcase),
"version": [1, 0, 0],
"min_engine_version": [1, 18, 3]
},
"modules": [
{
"description": "Adds 3D items for use with a Geyser proxy",
"type": "resources",
"uuid": ($uuid2 | ascii_downcase),
"version": [1, 0, 0]
}
]
}
' | sponge ./target/rp/manifest.json
# generate bp manifest.json
status_message process "Generating behavior pack manifest"
jq -c --arg pack_desc "${pack_desc}" --arg uuid1 "${uuid1}" --arg uuid3 "${uuid3}" --arg uuid4 "${uuid4}" -n '
{
"format_version": 2,
"header": {
"description": "Adds 3D items for use with a Geyser proxy",
"name": $pack_desc,
"uuid": ($uuid3 | ascii_downcase),
"version": [1, 0, 0],
"min_engine_version": [ 1, 18, 3]
},
"modules": [
{
"description": "Adds 3D items for use with a Geyser proxy",
"type": "data",
"uuid": ($uuid4 | ascii_downcase),
"version": [1, 0, 0]
}
],
"dependencies": [
{
"uuid": ($uuid1 | ascii_downcase),
"version": [1, 0, 0]
}
]
}
' | sponge ./target/bp/manifest.json
# generate rp terrain_texture.json
status_message process "Generating resource pack terrain texture definition"
jq -nc '
{
"resource_pack_name": "geyser_custom",
"texture_name": "atlas.terrain",
"texture_data": {
}
}
' | sponge ./target/rp/textures/terrain_texture.json
# generate rp item_texture.json
status_message process "Generating resource pack item texture definition"
jq -nc '
{
"resource_pack_name": "geyser_custom",
"texture_name": "atlas.items",
"texture_data": {}
}
' | sponge ./target/rp/textures/item_texture.json
status_message process "Generating resource pack disabling animation"
# generate our disabling animation
jq -nc '
{
"format_version": "1.8.0",
"animations": {
"animation.geyser_custom.disable": {
"loop": true,
"override_previous_animation": true,
"bones": {
"geyser_custom": {
"scale": 0
}
}
}
}
}
' | sponge ./target/rp/animations/geyser_custom/animation.geyser_custom.disable.json
# DO DEFAULT ASSETS HERE!!
# get the current default textures and merge them with our rp
if [[ ${fallback_pack} != none ]] && [[ ! -f default_assets.zip ]]
then
status_message process "Now downloading the fallback resource pack:"
printf "\e[3m\e[37m"
echo
COLUMNS=$COLUMNS-1 curl --no-styled-output -#L -o default_assets.zip https://github.com/InventivetalentDev/minecraft-assets/zipball/refs/tags/${default_asset_version:=1.19.2}
echo
printf "${C_CLOSE}"
status_message completion "Fallback resources downloaded"
fi
if [[ ${fallback_pack} != null && ${fallback_pack} != none ]]
then
printf "\e[3m\e[37m"
echo
COLUMNS=$COLUMNS-1 curl --no-styled-output -#L -o provided_assets.zip "${fallback_pack}"
echo
printf "${C_CLOSE}"
status_message completion "Provided resources downloaded"
mkdir ./providedassetholding
unzip -n -q -d ./providedassetholding provided_assets.zip "assets/**"
status_message completion "Provided resources decompressed"
cp -n -r "./providedassetholding/assets"/** './assets/'
status_message completion "Provided resources merged with target pack"
fi
if [[ ${fallback_pack} != none ]]
then
root_folder=($(unzip -Z -1 default_assets.zip | head -1))
mkdir ./defaultassetholding
unzip -n -q -d ./defaultassetholding default_assets.zip "${root_folder}assets/minecraft/textures/**/*"
unzip -n -q -d ./defaultassetholding default_assets.zip "${root_folder}assets/minecraft/models/**/*"
status_message completion "Fallback resources decompressed"
mkdir -p './assets/minecraft/textures/'
cp -n -r "./defaultassetholding/${root_folder}assets/minecraft/textures"/* './assets/minecraft/textures/'
cp -n -r "./defaultassetholding/${root_folder}assets/minecraft/models"/* './assets/minecraft/models/'
status_message completion "Fallback resources merged with target pack"
rm -rf defaultassetholding
#rm -f default_assets.zip
status_message critical "Extraneous fallback resources deleted\n"
fi
# generate a fallback texture
convert -size 16x16 xc:\#FFFFFF ./assets/minecraft/textures/0.png
# make sure we crop all mcmeta associated png files
status_message process "Cropping animated textures"
for i in $(find ./assets/**/textures -type f -name "*.mcmeta" | sed 's/\.mcmeta//'); do
convert ${i} -set option:distort:viewport "%[fx:min(w,h)]x%[fx:min(w,h)]" -distort affine "0,0 0,0" -define png:format=png8 -clamp ${i} 2> /dev/null
done
status_message completion "Initial pack setup complete\n"
jq -r '.[] | select(.parent != null) | [.path, .geyserID, .parent, .namespace, .model_path, .model_name, .path_hash] | @tsv | gsub("\\t";",")' config.json | sponge pa.csv
_start=1
_end="$(jq -r '(. | length) + ([.[] | select(.parent != null)] | length)' config.json)"
cur_pos=0
function ProgressBar {
let _progress=(${1}*100/${2}*100)/100
let _done=(${_progress}*6)/10
let _left=60-$_done
_fill=$(printf "%${_done}s")
_empty=$(printf "%${_left}s")
printf "\r\e[37m█\e[m \e[37m${_fill// /█}\e[m\e[37m${_empty// /•}\e[m \e[37m█\e[m \e[33m${_progress}\e[m\n"
}
# first, deal with parented models
while IFS=, read -r file gid parental namespace model_path model_name path_hash
do
cur_pos=$((cur_pos+1))
elements="$(jq -rc '.elements' ${file} | tee elements.temp)"
element_parent=${file}
textures="$(jq -rc '.textures' ${file} | tee textures.temp)"
display="$(jq -rc '.display' ${file} | tee display.temp)"
status_message process "Locating parental info for child model with GeyserID ${gid}"
# itterate through parented models until they all have geometry, display, and textures
until [[ ${elements} != null && ${textures} != null && ${display} != null ]] || [[ ${parental} = "./assets/minecraft/models/builtin/generated.json" ]] || [[ ${parental} = null ]]
do
if [[ ${elements} = null ]]
then
elements="$(jq -rc '.elements' ${parental} 2> /dev/null | tee elements.temp || (echo && echo null))"
element_parent=${parental}
fi
if [[ ${textures} = null ]]
then
textures="$(jq -rc '.textures' ${parental} 2> /dev/null | tee textures.temp || (echo && echo null))"
fi
if [[ ${display} = null ]]
then
display="$(jq -rc '.display' ${parental} 2> /dev/null | tee display.temp || (echo && echo null))"
fi
parental="$(jq -rc 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; ("./assets/" + (.parent? | namespace) + "/models/" + ((.parent? // empty) | sub("(.*?)\\:"; "")) + ".json") // "null"' ${parental} 2> /dev/null || (echo && echo null))"
texture_0="$(jq -rc 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; ("./assets/" + ([.[]][0]? | namespace) + "/textures/" + (([.[]][0]? // empty) | sub("(.*?)\\:"; "")) + ".png") // "null"' textures.temp)"
done
# if we can, generate a model now
if [[ ${elements} != null && ${textures} != null ]]
then
jq -n --slurpfile jelements elements.temp --slurpfile jtextures textures.temp --slurpfile jdisplay display.temp '
{
"textures": ($jtextures[]),
"elements": ($jelements[])
} + (if $jdisplay then ({"display": ($jdisplay[])}) else {} end)
' | sponge ${file}
jq --arg gid "${gid}" '.[$gid].generated |= "false"' config.json | sponge config.json
status_message completion "Located all parental info for Child ${gid}"
ProgressBar ${cur_pos} ${_end}
echo
# check if this is a 2d item dervived from ./assets/minecraft/models/builtin/generated
elif [[ ${textures} != null && ${parental} = "./assets/minecraft/models/builtin/generated.json" && -f "${texture_0}" ]]
then
jq -n --slurpfile jelements elements.temp --slurpfile jtextures textures.temp --slurpfile jdisplay display.temp '
{
"textures": ([$jtextures[]][0])
} + (if $jdisplay then ({"display": ($jdisplay[])}) else {} end)
' | sponge ${file}
jq --arg gid "${gid}" '.[$gid].generated |= "true"' config.json | sponge config.json
# copy texture directly to the rp
mkdir -p "./target/rp/textures/geyser/geyser_custom/${namespace}/${model_path}"
cp "${texture_0}" "./target/rp/textures/geyser/geyser_custom/${namespace}/${model_path}/${model_name}.png"
# add texture to item atlas
jq --arg path_hash "${path_hash}" --arg namespace "${namespace}" --arg model_path "${model_path}" --arg model_name "${model_name}" '.texture_data += {($path_hash): {"textures": ("textures/geyser/geyser_custom/" + $namespace + "/" + $model_path + "/" + $model_name)}}' ./target/rp/textures/item_texture.json | sponge ./target/rp/textures/item_texture.json
status_message completion "Located all parental info for 2D Child ${gid}"
ProgressBar ${cur_pos} ${_end}
echo
# otherwise, remove it from our config
else
status_message plain "Suitable parent information was not availbile for ${gid}..."
status_message critical "Deleting ${gid} from config"
ProgressBar ${cur_pos} ${_end}
echo
jq --arg gid "${gid}" 'del(.[$gid])' config.json | sponge config.json
fi
done < pa.csv
status_message process "Compiling final model list"
# get our final 3d model list from the config
model_list=( $(jq -r '.[] | select(.generated == "false") | .path' config.json) )
# get our final texture list to be atlased
# get a bash array of all texture files in our resource pack
status_message process "Generating an array of all model PNG files to crosscheck with our atlas"
jq -n '$ARGS.positional' --args $(find ./assets/**/textures -type f -name '*.png') | sponge all_textures.temp
# get bash array of all texture files listed in our models
status_message process "Generating union atlas arrays for all model textures"
jq -s '
def namespace:
if contains(":") then sub("\\:(.+)"; "") else "minecraft" end;
[.[]| [.textures[]?] | unique]
| map(map("./assets/" + (. | namespace) + "/textures/" + (. | sub("(.*?)\\:"; "")) + ".png"))
' ${model_list[@]} | sponge union_atlas.temp
jq '
def intersects(a;b): any(a[]; . as $x | any(b[]; . == $x));
def mapatlas(set):
(set | unique) as $unique_set
| (map(if intersects(.; $unique_set) then . else empty end) | add + $unique_set | unique) as $new_set
| map(if intersects(.; $new_set) then empty else . end) + [$new_set];
[["./assets/minecraft/textures/0.png"]] +
reduce .[] as $entry ([]; mapatlas($entry))
' union_atlas.temp | sponge union_atlas.temp
total_union_atlas=($(jq -r 'length - 1' union_atlas.temp))
mkdir spritesheet
status_message process "Generating $((1+${total_union_atlas})) sprite sheets..."
for i in $(seq 0 ${total_union_atlas})
do
# find the union of all texture files listed in this atlas and all texture files in our resource pack
texture_list=( $(jq -s --arg index "${i}" -r '(.[1][($index | tonumber)] - .[0] | length > 0) as $fallback_needed | ((.[1][($index | tonumber)] - (.[1][($index | tonumber)] - .[0])) + (if $fallback_needed then ["./assets/minecraft/textures/0.png"] else [] end)) | .[]' all_textures.temp union_atlas.temp) )
status_message process "Generating sprite sheet ${i} of ${total_union_atlas}"
spritesheet-js -f json --name spritesheet/${i} --fullpath ${texture_list[@]} > /dev/null 2>&1
jq --arg atlas_index "${i}" '.texture_data += {("gmdl_atlas_" + $atlas_index): {"textures": ("textures/geyser/geyser_custom/" + $atlas_index)}}' ./target/rp/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json
done
status_message completion "All sprite sheets generated"
mv spritesheet/*.png ./target/rp/textures/geyser/geyser_custom
# begin conversion
jq -r '.[] | [.path, .geyserID, .generated, .namespace, .model_path, .model_name, .path_hash] | @tsv | gsub("\\t";",")' config.json | sponge all.csv
while IFS=, read -r file gid generated namespace model_path model_name path_hash
do
convert_model () {
local file=${1}
local gid=${2}
local generated=${3}
local namespace=${4}
local model_path=${5}
local model_name=${6}
local path_hash=${7}
# find which texture atlas we will be using if not generated
if [[ ${generated} = "false" ]]
then
local atlas_index=$(jq -r -s 'def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end; def intersects(a;b): any(a[]; . as $x | any(b[]; . == $x)); (.[0] | [.textures[]] | map("./assets/" + (. | namespace) + "/textures/" + (. | sub("(.*?)\\:"; "")) + ".png")) as $inp | [(.[1] | (map(if intersects(.;$inp) then . else empty end)[])) as $entry | .[1] | to_entries[] | select(.value == $entry).key][0] // 0' ${file} union_atlas.temp)
else
local atlas_index=0
fi
status_message process "Starting conversion of model with GeyserID ${gid}"
mkdir -p ./target/rp/models/blocks/geyser_custom/${namespace}/${model_path}
jq --slurpfile atlas spritesheet/${atlas_index}.json --arg generated "${generated}" --arg binding "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)" --arg path_hash "${path_hash}" -c '
.textures as $texture_list |
def namespace: if contains(":") then sub("\\:(.+)"; "") else "minecraft" end;
def totexture($input): ($texture_list[($input[1:])]? // ([$texture_list[]][0]));
def topath($input): ("./assets/" + ($input | namespace) + "/textures/" + ($input | sub("(.*?)\\:"; "")) + ".png");
def texturedata($input): $atlas[] | .frames | (.[topath(totexture($input))] // ."./assets/minecraft/textures/0.png");
def roundit: (.*10000 | round) / 10000;
def element_array:
if .elements then (.elements | map({
"origin": [((-.to[0] + 8) | roundit), ((.from[1]) | roundit), ((.from[2] - 8) | roundit)],
"size": [((.to[0] - .from[0]) | roundit), ((.to[1] - .from[1]) | roundit), ((.to[2] - .from[2]) | roundit)],
"rotation": (if (.rotation.axis) == "x" then [(.rotation.angle | tonumber * -1), 0, 0] elif (.rotation.axis) == "y" then [0, (.rotation.angle | tonumber * -1), 0] elif (.rotation.axis) == "z" then [0, 0, (.rotation.angle | tonumber)] else null end),
"pivot": (if .rotation.origin then [((- .rotation.origin[0] + 8) | roundit), (.rotation.origin[1] | roundit), ((.rotation.origin[2] - 8) | roundit)] else null end),
"uv": (
def uv_calc($input):
(if (.faces | .[$input]) then
(.faces | .[$input].texture) as $input_n
| ( (((((.faces | .[$input].uv[0]) * (texturedata($input_n) | .frame.w) * 0.0625) + (texturedata($input_n) | .frame.x)) * (16 / ($atlas[] | .meta.size.w))) ) ) as $fn0
| ( (((((.faces | .[$input].uv[1]) * (texturedata($input_n) | .frame.h) * 0.0625) + (texturedata($input_n) | .frame.y)) * (16 / ($atlas[] | .meta.size.h))) ) ) as $fn1
| ( (((((.faces | .[$input].uv[2]) * (texturedata($input_n) | .frame.w) * 0.0625) + (texturedata($input_n) | .frame.x)) * (16 / ($atlas[] | .meta.size.w))) ) ) as $fn2
| ( (((((.faces | .[$input].uv[3]) * (texturedata($input_n) | .frame.h) * 0.0625) + (texturedata($input_n) | .frame.y)) * (16 / ($atlas[] | .meta.size.h))) ) ) as $fn3
| (($fn2 - $fn0) as $num | [([-1, $num] | max), 1] | min) as $x_sign
| (($fn3 - $fn1) as $num | [([-1, $num] | max), 1] | min) as $y_sign |
(if ($input == "up" or $input == "down") then {
"uv": [(($fn2 - (0.016 * $x_sign)) | roundit), (($fn3 - (0.016 * $y_sign)) | roundit)],
"uv_size": [((($fn0 - $fn2) + (0.016 * $x_sign)) | roundit), ((($fn1 - $fn3) + (0.016 * $y_sign)) | roundit)]
} else {
"uv": [(($fn0 + (0.016 * $x_sign)) | roundit), (($fn1 + (0.016 * $y_sign)) | roundit)],
"uv_size": [((($fn2 - $fn0) - (0.016 * $x_sign)) | roundit), ((($fn3 - $fn1) - (0.016 * $y_sign)) | roundit)]
} end) else null end);
{
"north": uv_calc("north"),
"south": uv_calc("south"),
"east": uv_calc("east"),
"west": uv_calc("west"),
"up": uv_calc("up"),
"down": uv_calc("down")
})
}) | walk( if type == "object" then with_entries(select(.value != null)) else . end)) else {} end
;
def pivot_groups:
if .elements then ((element_array) as $element_array |
[[.elements[].rotation] | unique | .[] | select (.!=null)]
| map((
[((- .origin[0] + 8) | roundit), (.origin[1] | roundit), ((.origin[2] - 8) | roundit)] as $i_piv |
(if (.axis) == "x" then [(.angle | tonumber * -1), 0, 0] elif (.axis) == "y" then [0, (.angle | tonumber * -1), 0] else [0, 0, (.angle | tonumber)] end) as $i_rot |
{
"parent": "geyser_custom_z",
"pivot": ($i_piv),
"rotation": ($i_rot),
"cubes": [($element_array | .[] | select(.rotation == $i_rot and .pivot == $i_piv))]
}))) else {} end
;
{
"format_version": "1.16.0",
"minecraft:geometry": [{
"description": {
"identifier": ("geometry.geyser_custom." + ($path_hash)),
"texture_width": 16,
"texture_height": 16,
"visible_bounds_width": 4,
"visible_bounds_height": 4.5,
"visible_bounds_offset": [0, 0.75, 0]
},
"bones": ([{
"name": "geyser_custom",
"binding": $binding,
"pivot": [0, 8, 0]
}, {
"name": "geyser_custom_x",
"parent": "geyser_custom",
"pivot": [0, 8, 0]
}, {
"name": "geyser_custom_y",
"parent": "geyser_custom_x",
"pivot": [0, 8, 0]
},
if $generated == "true" then ({
"name": "geyser_custom_z",
"parent": "geyser_custom_y",
"pivot": [0, 8, 0],
"texture_meshes": ([{"texture": "default", "position": [0, 8, 0], "rotation": [90, 0, -180], "local_pivot": [8, 0.5, 8]}])
}) else ({
"name": "geyser_custom_z",
"parent": "geyser_custom_y",
"pivot": [0, 8, 0],
"cubes": ([(element_array | .[] | select(.rotation == null))])
}) end] + (pivot_groups | map(del(.cubes[].rotation)) | to_entries | map( (.value.name = "rot_\(1+.key)" ) | .value)))
}]
}
' ${file} | sponge ./target/rp/models/blocks/geyser_custom/${namespace}/${model_path}/${model_name}.json
# generate our rp animations via display settings
mkdir -p ./target/rp/animations/geyser_custom/${namespace}/${model_path}
jq -c --arg path_hash "${path_hash}" '
{
"format_version": "1.8.0",
"animations": {
("animation.geyser_custom." + ($path_hash) + ".thirdperson_main_hand"): {
"loop": true,
"bones": {
"geyser_custom_x": (if .display.thirdperson_righthand then {
"rotation": (if .display.thirdperson_righthand.rotation then [(- .display.thirdperson_righthand.rotation[0]), 0, 0] else null end),
"position": (if .display.thirdperson_righthand.translation then [(- .display.thirdperson_righthand.translation[0]), (.display.thirdperson_righthand.translation[1]), (.display.thirdperson_righthand.translation[2])] else null end),
"scale": (if .display.thirdperson_righthand.scale then [(.display.thirdperson_righthand.scale[0]), (.display.thirdperson_righthand.scale[1]), (.display.thirdperson_righthand.scale[2])] else null end)
} else null end),
"geyser_custom_y": (if .display.thirdperson_righthand.rotation then {
"rotation": (if .display.thirdperson_righthand.rotation then [0, (- .display.thirdperson_righthand.rotation[1]), 0] else null end)
} else null end),
"geyser_custom_z": (if .display.thirdperson_righthand.rotation then {
"rotation": [0, 0, (.display.thirdperson_righthand.rotation[2])]
} else null end),
"geyser_custom": {
"rotation": [90, 0, 0],
"position": [0, 13, -3]
}
}
},
("animation.geyser_custom." + ($path_hash) + ".thirdperson_off_hand"): {
"loop": true,
"bones": {
"geyser_custom_x": (if .display.thirdperson_lefthand then {
"rotation": (if .display.thirdperson_lefthand.rotation then [(- .display.thirdperson_lefthand.rotation[0]), 0, 0] else null end),
"position": (if .display.thirdperson_lefthand.translation then [(- .display.thirdperson_lefthand.translation[0]), (.display.thirdperson_lefthand.translation[1]), (.display.thirdperson_lefthand.translation[2])] else null end),
"scale": (if .display.thirdperson_lefthand.scale then [(.display.thirdperson_lefthand.scale[0]), (.display.thirdperson_lefthand.scale[1]), (.display.thirdperson_lefthand.scale[2])] else null end)
} else null end),
"geyser_custom_y": (if .display.thirdperson_lefthand.rotation then {
"rotation": (if .display.thirdperson_lefthand.rotation then [0, (- .display.thirdperson_lefthand.rotation[1]), 0] else null end)
} else null end),
"geyser_custom_z": (if .display.thirdperson_lefthand.rotation then {
"rotation": [0, 0, (.display.thirdperson_lefthand.rotation[2])]
} else null end),
"geyser_custom": {
"rotation": [90, 0, 0],
"position": [0, 13, -3]
}
}
},
("animation.geyser_custom." + ($path_hash) + ".head"): {
"loop": true,
"bones": {
"geyser_custom_x": {
"rotation": (if .display.head.rotation then [(- .display.head.rotation[0]), 0, 0] else null end),
"position": (if .display.head.translation then [(- .display.head.translation[0] * 0.625), (.display.head.translation[1] * 0.625), (.display.head.translation[2] * 0.625)] else null end),
"scale": (if .display.head.scale then (.display.head.scale | map(. * 0.625)) else 0.625 end)
},
"geyser_custom_y": (if .display.head.rotation then {
"rotation": [0, (- .display.head.rotation[1]), 0]
} else null end),
"geyser_custom_z": (if .display.head.rotation then {
"rotation": [0, 0, (.display.head.rotation[2])]
} else null end),
"geyser_custom": {
"position": [0, 19.5, 0]
}
}
},
("animation.geyser_custom." + ($path_hash) + ".firstperson_main_hand"): {
"loop": true,
"bones": {
"geyser_custom": {
"rotation": [90, 60, -40],
"position": [4, 10, 4],
"scale": 1.5
},
"geyser_custom_x": {
"position": (if .display.firstperson_righthand.translation then [(- .display.firstperson_righthand.translation[0]), (.display.firstperson_righthand.translation[1]), (- .display.firstperson_righthand.translation[2])] else null end),
"rotation": (if .display.firstperson_righthand.rotation then [(- .display.firstperson_righthand.rotation[0]), 0, 0] else [0.1, 0.1, 0.1] end),
"scale": (if .display.firstperson_righthand.scale then (.display.firstperson_righthand.scale) else null end)
},
"geyser_custom_y": (if .display.firstperson_righthand.rotation then {
"rotation": [0, (- .display.firstperson_righthand.rotation[1]), 0]
} else null end),
"geyser_custom_z": (if .display.firstperson_righthand.rotation then {
"rotation": [0, 0, (.display.firstperson_righthand.rotation[2])]
} else null end)
}
},
("animation.geyser_custom." + ($path_hash) + ".firstperson_off_hand"): {
"loop": true,
"bones": {
"geyser_custom": {
"rotation": [90, 60, -40],
"position": [4, 10, 4],
"scale": 1.5
},
"geyser_custom_x": {
"position": (if .display.firstperson_lefthand.translation then [(.display.firstperson_lefthand.translation[0]), (.display.firstperson_lefthand.translation[1]), (- .display.firstperson_lefthand.translation[2])] else null end),
"rotation": (if .display.firstperson_lefthand.rotation then [(- .display.firstperson_lefthand.rotation[0]), 0, 0] else [0.1, 0.1, 0.1] end),
"scale": (if .display.firstperson_lefthand.scale then (.display.firstperson_lefthand.scale) else null end)
},
"geyser_custom_y": (if .display.firstperson_lefthand.rotation then {
"rotation": [0, (- .display.firstperson_lefthand.rotation[1]), 0]
} else null end),
"geyser_custom_z": (if .display.firstperson_lefthand.rotation then {
"rotation": [0, 0, (.display.firstperson_lefthand.rotation[2])]
} else null end)
}
}
}
} | walk( if type == "object" then with_entries(select(.value != null)) else . end)
' ${file} | sponge ./target/rp/animations/geyser_custom/${namespace}/${model_path}/animation.${model_name}.json
# generate our bp block definition if this is a 3D item
if [[ ${generated} = false ]]
then
mkdir -p ./target/bp/blocks/geyser_custom/${namespace}/${model_path}
jq -c -n --arg atlas_index "${atlas_index}" --arg block_material "${block_material}" --arg path_hash "${path_hash}" '
{
"format_version": "1.16.100",
"minecraft:block": {
"description": {
"identifier": ("geyser_custom:" + $path_hash)
},
"components": {
"minecraft:material_instances": {
"*": {
"texture": ("gmdl_atlas_" + $atlas_index),
"render_method": $block_material,
"face_dimming": false,
"ambient_occlusion": false
}
},
"minecraft:geometry": ("geometry.geyser_custom." + $path_hash),
"minecraft:placement_filter": {
"conditions": [
{
"allowed_faces": [
],
"block_filter": [
]
}
]
}
}
}
}
' | sponge ./target/bp/blocks/geyser_custom/${namespace}/${model_path}/${model_name}.json
# generate our bp item definition if this is a 2D item
else
mkdir -p ./target/bp/items/geyser_custom/${namespace}/${model_path}
jq -c -n --arg path_hash "${path_hash}" '
{
"format_version": "1.16.100",
"minecraft:item": {
"description": {
"identifier": ("geyser_custom:" + $path_hash),
"category": "items"
},
"components": {
"minecraft:icon": {
"texture": $path_hash
}
}
}
}
' | sponge ./target/bp/items/geyser_custom/${namespace}/${model_path}/${model_name}.json
fi
# generate our rp attachable definition
mkdir -p ./target/rp/attachables/geyser_custom/${namespace}/${model_path}
jq -c -n --arg generated "${generated}" --arg atlas_index "${atlas_index}" --arg attachable_material "${attachable_material}" --arg v_main "v.main_hand = c.item_slot == 'main_hand';" --arg v_off "v.off_hand = c.item_slot == 'off_hand';" --arg v_head "v.head = c.item_slot == 'head';" --arg path_hash "${path_hash}" --arg namespace "${namespace}" --arg model_path "${model_path}" --arg model_name "${model_name}" '
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": ("geyser_custom:" + $path_hash),
"materials": {
"default": $attachable_material,
"enchanted": $attachable_material
},
"textures": {
"default": (if $generated == "true" then ("textures/geyser/geyser_custom/" + $namespace + "/" + $model_path + "/" + $model_name) else ("textures/geyser/geyser_custom/" + $atlas_index) end),
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": ("geometry.geyser_custom." + $path_hash)
},
"scripts": {
"pre_animation": [$v_main, $v_off, $v_head],
"animate": [
{"thirdperson_main_hand": "v.main_hand && !c.is_first_person"},
{"thirdperson_off_hand": "v.off_hand && !c.is_first_person"},
{"thirdperson_head": "v.head && !c.is_first_person"},
{"firstperson_main_hand": "v.main_hand && c.is_first_person"},
{"firstperson_off_hand": "v.off_hand && c.is_first_person"},
{"firstperson_head": "c.is_first_person && v.head"}
]
},
"animations": {
"thirdperson_main_hand": ("animation.geyser_custom." + $path_hash + ".thirdperson_main_hand"),
"thirdperson_off_hand": ("animation.geyser_custom." + $path_hash + ".thirdperson_off_hand"),
"thirdperson_head": ("animation.geyser_custom." + $path_hash + ".head"),
"firstperson_main_hand": ("animation.geyser_custom." + $path_hash + ".firstperson_main_hand"),
"firstperson_off_hand": ("animation.geyser_custom." + $path_hash + ".firstperson_off_hand"),
"firstperson_head": "animation.geyser_custom.disable"
},
"render_controllers": [ "controller.render.item_default" ]
}
}
}
' | sponge ./target/rp/attachables/geyser_custom/${namespace}/${model_path}/${model_name}.attachable.json
# progress
echo >> count.csv
local tot_pos=$((cur_pos + $(wc -l < count.csv)))
status_message completion "${gid} converted\n$(ProgressBar ${tot_pos} ${_end})"
echo
}
wait_for_jobs
convert_model ${file} ${gid} ${generated} ${namespace} ${model_path} ${model_name} ${path_hash} &
done < all.csv
wait # wait for all the jobs to finish
# write lang file US
status_message process "Writing en_US and en_GB lang files"
mkdir ./target/rp/texts
jq -r '
def format: (.[0:1] | ascii_upcase ) + (.[1:] | gsub( "_(?<a>[a-z])"; (" " + .a) | ascii_upcase));
.[]|"\("item.geyser_custom:" + .path_hash + ".name")=\(.item | format)"
' config.json | sponge ./target/rp/texts/en_US.lang
# copy US lang to GB
cp ./target/rp/texts/en_US.lang ./target/rp/texts/en_GB.lang
# write supported languages file
jq -n '["en_US","en_GB"]' | sponge ./target/rp/texts/languages.json
status_message completion "en_US and en_GB lang files written\n"
# apply image compression if we can
#if command -v pngquant >/dev/null 2>&1 ; then
# status_message completion "Optional dependency pngquant detected"
# status_message process "Attempting image compression"
# pngquant -f --skip-if-larger --ext .png --strip ./target/rp/textures/geyser/geyser_custom/*.png
# status_message completion "Image compression complete"
# echo
#fi
# Ensure images are in the correct color space
status_message process "Setting all images to png8"
find ./target/rp/textures/geyser/geyser_custom -name '*.png' -exec mogrify -define png:format=png8 {} +
status_message completion "All images set to png8"
# attempt to merge with existing pack if input was provided
if test -f ${merge_input}; then
mkdir inputbedrockpack
status_message process "Decompressing input bedrock pack"
unzip -q ${merge_input} -d ./inputbedrockpack
status_message process "Merging input bedrock pack with generated bedrock assets"
cp -n -r "./inputbedrockpack"/* './target/rp/'
if test -f ./inputbedrockpack/textures/terrain_texture.json; then
status_message process "Merging terrain texture files"
jq -s '
{
"resource_pack_name": "geyser_custom",
"texture_name": "atlas.terrain",
"texture_data": (.[1].texture_data + .[0].texture_data)
}
' ./target/rp/textures/terrain_texture.json ./inputbedrockpack/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json
fi
if test -f ./inputbedrockpack/textures/item_texture.json; then
status_message process "Merging item texture files"
jq -s '
{
"resource_pack_name": "geyser_custom",
"texture_name": "atlas.items",
"texture_data": (.[1].texture_data + .[0].texture_data)
}
' ./target/rp/textures/item_texture.json ./inputbedrockpack/textures/item_texture.json | sponge ./target/rp/textures/item_texture.json
fi
if test -f ./inputbedrockpack/texts/languages.json; then
status_message process "Merging languages file"
jq -s '.[0] + .[1] | unique' | sponge ./target/rp/texts/languages.json
fi
if test -f ./inputbedrockpack/texts/en_US.lang; then
status_message process "Merging en_US lang file"
cat ./inputbedrockpack/texts/en_US.lang >> ./target/rp/texts/en_US.lang
fi
if test -f ./inputbedrockpack/texts/en_GB.lang; then
status_message process "Merging en_GB lang file"
cat ./inputbedrockpack/texts/en_GB.lang >> ./target/rp/texts/en_GB.lang
fi
status_message critical "Deleting input bedrock pack scratch direcotry"
rm -rf inputbedrockpack
status_message completion "Input bedrock pack merged with generated assets\n"
fi
#status_message critical "Deleting unused entries from config"
# jq 'map_values(del(.path, .element_parent, .parent, .geyserID))' config.json | sponge config.json
status_message process "Creating Geyser mappings in target directory"
echo
jq '
([map(
{
("minecraft:" + .item): [
{
"name": .path_hash,
"allow_offhand": true,
"icon": (if .generated == "true" then .path_hash else .bedrock_icon.icon end)
}
+ (if (.generated == "false") then {"frame": (.bedrock_icon.frame)} else {} end)
+ (if .nbt.CustomModelData then {"custom_model_data": (.nbt.CustomModelData)} else {} end)
+ (if .nbt.Damage then {"damage_predicate": (.nbt.Damage)} else {} end)
+ (if .nbt.Unbreakable then {"unbreakable": (.nbt.Unbreakable)} else {} end)
]
}
)
| map(to_entries[])
| group_by(.key)[]
| {(.[0].key) : map(.value) | add}] | add) as $mappings
| {
"format_version": "1",
"items": $mappings
}
' config.json | sponge ./target/geyser_mappings.json
# Add sprites if sprites.json exists in the root pack
if [ -f sprites.json ]; then
status_message process "Adding provided sprite paths from sprites.json"
jq -r '
to_entries
| map(.key as $item | .value | map(. += {"item": $item}))
| add[]
| [((.item | split(":")[-1]) + "_c" + (.custom_model_data | tostring) + "_d" + (.damage_predicate | tostring) + "_u" + (.unbreakable | tostring)), .sprite]
| @tsv
| gsub("\\t";",")
' sprites.json > sprites.csv
while IFS=, read -r predicate icon
do write_hash "${predicate}" "${icon}" "sprite_hashes.csv" &
done < sprites.csv > /dev/null
jq -cR 'split(",")' sprite_hashes.csv | jq -s 'map({("gmdl_" + .[1]): {"textures": .[0]}}) | add' > sprite_hashmap.json
jq -s '
.[0] as $icon_sprites
| .[1]
| .texture_data += $icon_sprites
' sprite_hashmap.json ./target/rp/textures/item_texture.json | sponge ./target/rp/textures/item_texture.json
jq -s '
{
"format_version": "1",
"items":
((.[0] | keys | map({(.): (.)}) | add) as $sprites | .[1].items | to_entries | map(
(.key | split(":")[1]) as $item
| .value | {("minecraft:" + $item): (map(
.name as $name
| .icon as $icon
| .icon = ($sprites[($name)] // $icon)
))}
) | add)
}
' sprite_hashmap.json ./target/geyser_mappings.json | sponge ./target/geyser_mappings.json
rm -f sprites.json && rm -f sprites.csv && rm -f sprite_hashes.csv && rm -f sprite_hashmap.json
fi
# cleanup
status_message critical "Deleting scratch files"
rm -rf spritesheet && rm -rf assets && rm -f pack.mcmeta && rm -f pack.png && rm -f parents.json && rm -f all.csv && rm -f pa.csv && rm -f *.temp && rm -f item_mappings.json && rm -f item_texture.json && rm paths.csv && rm hashes.csv && rm hashmap.json && rm count.csv
status_message process "Compressing output packs"
mkdir ./target/packaged
cd ./target/rp > /dev/null && zip -rq8 geyser_resources_preview.mcpack . -x "*/.*" && cd ../.. > /dev/null && mv ./target/rp/geyser_resources_preview.mcpack ./target/packaged/geyser_resources_preview.mcpack
cd ./target/bp > /dev/null && zip -rq8 geyser_behaviors_preview.mcpack . -x "*/.*" && cd ../.. > /dev/null && mv ./target/bp/geyser_behaviors_preview.mcpack ./target/packaged/geyser_behaviors_preview.mcpack
cd ./target/packaged > /dev/null && zip -rq8 geyser_addon.mcaddon . -i "*_preview.mcpack" && cd ../.. > /dev/null
jq 'delpaths([paths | select(.[-1] | strings | startswith("gmdl_atlas_"))])' ./target/rp/textures/terrain_texture.json | sponge ./target/rp/textures/terrain_texture.json
cd ./target/rp > /dev/null && zip -rq8 geyser_resources.mcpack . -x "*/.*" && cd ../.. > /dev/null && mv ./target/rp/geyser_resources.mcpack ./target/packaged/geyser_resources.mcpack
mkdir ./target/unpackaged
mv ./target/rp ./target/unpackaged/rp && mv ./target/bp ./target/unpackaged/bp
echo
printf "\e[32m[+]\e[m \e[1m\e[37mConversion Process Complete\e[m\n\n\e[37mExiting...\e[m\n\n"