Last active
June 25, 2024 13:50
-
-
Save eingruenesbeb/72d57a260261e4b88a0ca16714dcda3b to your computer and use it in GitHub Desktop.
A "little" Bash script to generate an empty Minecraft datapack
This file contains 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
#!/bin/bash | |
# This script will generate an empty datapack with the basic files needed | |
# It prompts the user to input details like parent directory, datapack name, format, and description. | |
# global variables: use_gum, parent_dir, name, pack_format, description, selected_features, subdirectories | |
# Helper function to join array elements with a specified delimiter | |
function joinArrayWith() { | |
local delimiter="$1" | |
shift | |
local joined | |
joined=$(printf "%s${delimiter}" "$@") | |
echo "${joined%"${delimiter}"}" | |
} | |
function gatherPackFeatures() { | |
# Possible datapack-features by type: | |
local features_4=("Functions" "Structures" "Tags" "Advancements" "Item modifier" "Loot tables" "Predicates" "Recipes") | |
local features_5=("Dimensions" "Dimension types" "Trim materials" "Trim patterns" "Customized worldgeneration") | |
local features_10=("Chat types") | |
local features_12=("Damage types") | |
local features_34=("Banner patterns" "Wolf variants") | |
local features_42=("Enchantments" "Enchantment providers" "Paintings") | |
# Concatenate every available feature for the datapack format | |
local possible_features=("${features_4[@]}") | |
if [[ "$pack_format" -ge 5 ]]; then | |
possible_features+=("${features_5[@]}") | |
fi | |
if [[ "$pack_format" -ge 10 ]]; then | |
possible_features+=("${features_10[@]}") | |
fi | |
if [[ "$pack_format" -ge 12 ]]; then | |
possible_features+=("${features_12[@]}") | |
fi | |
if [[ "$pack_format" -ge 34 ]]; then | |
possible_features+=("${features_34[@]}") | |
fi | |
if [[ "$pack_format" -ge 42 ]]; then | |
possible_features+=("${features_42[@]}") | |
fi | |
selected_features=() | |
if $use_gum; then | |
echo "What will your datapack include?" | |
# The features the user wants to include in the datapack | |
local selected | |
selected=$(gum choose --no-limit "${possible_features[@]}") | |
IFS=$'\n' read -r -d '' -a selected_features <<< "$selected"$'\n' | |
else | |
echo "What will your datapack include? Answer with a newline-separated set of: $(joinArrayWith ', ' "${possible_features[@]}")" | |
echo "Press \`<ctrl>+D\` on a new line to confirm." | |
local line | |
while read -r line; do | |
selected_features+=("$line") | |
done | |
fi | |
} | |
function detectGum() { | |
if [[ "$(command -v gum)" ]]; then | |
use_gum=true | |
else | |
use_gum=false | |
fi | |
} | |
# Function to generate the pack.mcmeta file content with provided pack format and description. | |
function generatePackMeta() { | |
cat << EOF | |
{ | |
"pack": { | |
"pack_format": $1, | |
"description": "$2" | |
} | |
} | |
EOF | |
} | |
# Function to generate the load.json file content with the given namespace. | |
function generateLoadTag() { | |
cat << EOF | |
{ | |
"values": [ | |
"$1:load" | |
] | |
} | |
EOF | |
} | |
# Function to generate the tick.json file content with the given namespace. | |
function generateTickTag() { | |
cat << EOF | |
{ | |
"values": [ | |
"$1:main" | |
] | |
} | |
EOF | |
} | |
# Function to generate the content of the load.mcfunction file. | |
function generateLoadFunction() { | |
cat << EOF | |
# Write here, what your pack should do, when loaded: | |
EOF | |
} | |
# Function to generate the content of the main.mcfunction file. | |
function generateMainFunction() { | |
cat << EOF | |
# Write here, what your pack should do in each tick: | |
EOF | |
} | |
# Function to sanitize the name and convert it to a valid namespace | |
function sanitizeNamespace() { | |
local input="$1" | |
local sanitized="${input,,}" # Convert to lowercase | |
sanitized="${sanitized// /_}" # Replace spaces with underscores | |
echo "$sanitized" | |
} | |
# Function to sanitize the input for a valid file path descriptor. | |
function sanitizePathDescriptor() { | |
local input="$1" | |
local sanitized="${input//\\/}" # Remove backslashes (escape characters) | |
sanitized="${sanitized//[^[:alnum:]_\-\/. ]/}" # Remove non-alphanumeric, non-underscore, non-hyphen, non-slash, non-space, and non-dot characters | |
sanitized="${sanitized//\/\//\/}" # Replace double slashes with a single slash | |
echo "$sanitized" | |
} | |
# Function to sanitize the input for a valid directory name. | |
function sanitizeDirectoryname() { | |
local input="$1" | |
local sanitized="${input//\\/}" # Remove backslashes (escape characters) | |
sanitized="${sanitized//[^[:alnum:]_\- ]/}" # Remove non-alphanumeric, non-underscore, non-space, and non-hyphen characters | |
echo "$sanitized" | |
} | |
# Function to sanitize the input for safe insertion into a JSON file | |
function sanitizeDescription() { | |
local input="$1" | |
local sanitized | |
sanitized=$(printf '%s' "$input" | sed 's/\\/\\\\/g; s/"/\\"/g') | |
sanitized="${sanitized//$'\n'/\\n}" | |
echo "$sanitized" | |
} | |
function formatList { | |
local title="$1" | |
shift | |
local array=("$@") | |
local result="$title:\n" | |
for element in "${array[@]}"; do | |
result+="\t- $element\n" | |
done | |
echo -e "$result" | |
} | |
# Function to gather user input for the datapack details. | |
# Sets the following variables globally: parent_directory, name, pack_format, description | |
# The first argument is expected to be a string containing the descriptors of each input to be gathered (or none). | |
function gatherUserInput() { | |
# Look at next echo command message for details. | |
for to_gather in "$@"; do | |
if [[ "$to_gather" == "Parent directory" ]]; then | |
while true; do | |
echo 'Please enter the parent directory to place the project directory in. Leave blank to place it in the current working directory or, if this script is located inside a directory called "scripts", its parent.' | |
if "$use_gum"; then | |
parent_dir=$(gum input --placeholder "path/to/parent") | |
else | |
read -r parent_dir | |
fi | |
if [[ "$parent_dir" == "" || -z "$parent_dir" ]]; then | |
# No explicit parent path means to choose the directory the script is located in. | |
parent_dir=$(dirname "$0") | |
# If the directory of the script is called `scripts`, assume the script to be in a multi-project repo. | |
if [[ "$parent_dir" == *"/scripts" ]]; then | |
parent_dir=$(dirname "$parent_dir") | |
fi | |
break | |
elif [[ "$parent_dir" =~ [^\0]+ ]]; then | |
# An explicit path was given and it's valid under a UNIX-like system. | |
break | |
fi | |
echo "Invalid path!" | |
done | |
parent_dir=$(readlink -f "$parent_dir") | |
fi | |
# The name must be a valid name for a directory, because well... It's also the name for the directory housing the datapack-files. | |
if [[ $to_gather == "Name" ]]; then | |
echo "Please enter the Name of the new Datapack. (Note, that it must also be a valid directory name!)" | |
if $use_gum; then | |
name="$(gum input --placeholder "A valid directory name")" | |
else | |
read -r name | |
fi | |
while true; do | |
name=$(sanitizeDirectoryname "$name") | |
if [ -d "$parent_dir/$name" ]; then | |
echo "This already exists!" | |
if $use_gum; then | |
name="$(gum input --prompt "Please enter a different name or cancel: " --placeholder "A valid directory name")" | |
else | |
read -r name | |
fi | |
elif [ -z "$name" ]; then | |
echo "Invalid name!" | |
if $use_gum; then | |
name="$(gum input --prompt "Please enter a valid name or cancel: " --placeholder "A valid directory name")" | |
else | |
read -r name | |
fi | |
else | |
break | |
fi | |
done | |
fi | |
if [[ $to_gather == "Format" ]]; then | |
echo "Please enter the pack format. (You can consult https://minecraft.wiki/Pack_format for an up to date reference.)" | |
if $use_gum; then | |
pack_format="$(gum input --placeholder "A valid pack format number")" | |
else | |
read -r pack_format | |
fi | |
while true; do | |
if [[ "$pack_format" =~ ^[0-9]+$ ]]; then | |
if [[ ! "$pack_format" -lt 49 ]]; then # Should this script be available in a dedicated repository one day, the upper bound will be updated appropriately. | |
echo -e "Warning: The pack format is suspiciously high.\nThis warning can be ignored, when 48 is outdated as the highest pack format number." | |
fi | |
break | |
else | |
echo "Invalid Format!" | |
if $use_gum; then | |
pack_format="$(gum input --prompt "Please enter a valid pack format number: " --placeholder "A valid pack format number")" | |
else | |
read -r -p "Please enter a valid pack format number: " pack_format | |
fi | |
fi | |
done | |
fi | |
if [[ "$to_gather" == "Description" ]]; then | |
if $use_gum; then | |
echo "How would you describe the pack?" | |
description="$(gum write --placeholder "Description of the pack")" | |
else | |
local description_lines=() | |
echo "How would you describe the pack?" | |
echo "Press \`<ctrl>+D\` on a new line to confirm." | |
local line | |
while read -r line; do | |
description_lines+=("$line") | |
done | |
description=$(joinArrayWith '\n' "${description_lines[@]}") | |
echo "$description" | |
fi | |
description=$(sanitizeDescription "$description") | |
fi | |
if [[ $to_gather == "Features" ]]; then | |
# Sets selected_features | |
gatherPackFeatures | |
fi | |
done | |
} | |
# Function to gather user input and confirm before generating the datapack. | |
function gatherAndConfirm() { | |
local input_confirm | |
local required_input=("Parent directory" "Name" "Format" "Description" "Features") | |
while true; do | |
gatherUserInput "${required_input[@]}" | |
required_input=() | |
echo "You're about to create a new pack with the following attributes:" | |
echo -e "Parent directory: $parent_dir\nName: $name\nPack format: $pack_format\nDescription: $description\n$(formatList "Selected features" "${selected_features[@]}")" | |
if $use_gum; then | |
gum confirm "Is this correct?" && break || input_confirm="n" | |
else | |
echo "Is this correct? (y|n)" | |
read -r input_confirm | |
fi | |
while true; do | |
input_confirm="${input_confirm,,}" | |
if [[ $input_confirm =~ ^[yn]$ ]]; then | |
break | |
else | |
# Can only be reached, if gum is not installed. | |
echo 'Please answer with "y" or "n"!' | |
read -r input_confirm | |
fi | |
done | |
if [[ "$input_confirm" == "y" ]]; then | |
break | |
elif [[ "$input_confirm" == "n" ]]; then | |
if $use_gum; then | |
echo 'What information would you like to edit? Press "x" to select an item or "a" to select all.' | |
local selected | |
selected=$(gum choose --no-limit "Parent directory" "Name" "Format" "Description" "Features") | |
IFS=$'\n' read -r -d '' -a required_input <<< "$selected"$'\n' | |
else | |
echo "What information would you like to edit? Answer with a line separated set of: \"Parent directory\", \"Name\", \"Format\" and \"Description\"; Confirm with \`<Ctrl>+D\`." | |
local line | |
while read -r line; do | |
required_input+=("$line") | |
done | |
fi | |
fi | |
done | |
} | |
function setSubDirsNew() { | |
subdirectories=() | |
for feature in "${selected_features[@]}"; do | |
case $feature in | |
"Functions") | |
subdirectories+=("function") | |
;; | |
"Structures") | |
subdirectories+=("structure") | |
;; | |
"Tags") | |
subdirectories+=("tags") | |
;; | |
"Advancements") | |
subdirectories+=("advancement") | |
;; | |
"Item modifier") | |
subdirectories+=("item_modifier") | |
;; | |
"Loot tables") | |
subdirectories+=("loot_table") | |
;; | |
"Predicates") | |
subdirectories+=("predicate") | |
;; | |
"Recipes") | |
subdirectories+=("recipe") | |
;; | |
"Dimensions") | |
subdirectories+=("dimension") | |
;; | |
"Dimension types") | |
subdirectories+=("dimension_type") | |
;; | |
"Trim materials") | |
subdirectories+=("trim_material") | |
;; | |
"Trim patterns") | |
subdirectories+=("trim_pattern") | |
;; | |
"Chat types") | |
subdirectories+=("chat_type") | |
;; | |
"Damage types") | |
subdirectories+=("damage_type") | |
;; | |
"Banner patterns") | |
subdirectories+=("banner_pattern") | |
;; | |
"Wolf variants") | |
subdirectories+=("wolf_variant") | |
;; | |
"Enchantments") | |
subdirectories+=("enchantment") | |
;; | |
"Enchantment providers") | |
subdirectories+=("enchantment_provider") | |
;; | |
"Paintings") | |
subdirectories+=("painting_variant") | |
;; | |
"Customized worldgeneration") | |
subdirectories+=("worldgen/biome" "worldgen/configured_carver" "worldgen/configured_feature" "worldgen/density_function" "worldgen/noise" "worldgen/noise_settings" "worldgen/placed_feature" "worldgen/processor_list" "worldgen/structure" "worldgen/structure_set" "worldgen/template_pool" "worldgen/world_preset" "worldgen/flat_level_generator_preset" "worldgen/multi_noise_biome_noise_parameter_list") | |
;; | |
esac | |
done | |
} | |
function setSubDirsOld() { | |
subdirectories=() | |
for feature in "${selected_features[@]}"; do | |
case $feature in | |
"Functions") | |
subdirectories+=("functions") | |
;; | |
"Structures") | |
subdirectories+=("structures") | |
;; | |
"Tags") | |
subdirectories+=("tags") | |
;; | |
"Advancements") | |
subdirectories+=("advancements") | |
;; | |
"Item modifier") | |
subdirectories+=("item_modifiers") | |
;; | |
"Loot tables") | |
subdirectories+=("loot_tables") | |
;; | |
"Predicates") | |
subdirectories+=("predicates") | |
;; | |
"Recipes") | |
subdirectories+=("recipes") | |
;; | |
"Dimensions") | |
subdirectories+=("dimension") | |
;; | |
"Dimension types") | |
subdirectories+=("dimension_type") | |
;; | |
"Trim materials") | |
subdirectories+=("trim_material") | |
;; | |
"Trim patterns") | |
subdirectories+=("trim_pattern") | |
;; | |
"Chat types") | |
subdirectories+=("chat_type") | |
;; | |
"Damage types") | |
subdirectories+=("damage_type") | |
;; | |
"Banner patterns") | |
subdirectories+=("banner_pattern") | |
;; | |
"Wolf variants") | |
subdirectories+=("wolf_variant") | |
;; | |
"Enchantments") | |
subdirectories+=("enchantment") | |
;; | |
"Enchantment providers") | |
subdirectories+=("enchantment_provider") | |
;; | |
"Paintings") | |
subdirectories+=("painting_variant") | |
;; | |
"Customized worldgeneration") | |
subdirectories+=("worldgen/biome" "worldgen/configured_carver" "worldgen/configured_feature" "worldgen/density_function" "worldgen/noise" "worldgen/noise_settings" "worldgen/placed_feature" "worldgen/processor_list" "worldgen/structure" "worldgen/structure_set" "worldgen/template_pool" "worldgen/world_preset" "worldgen/flat_level_generator_preset" "worldgen/multi_noise_biome_noise_parameter_list") | |
;; | |
esac | |
done | |
} | |
# Function to generate the datapack based on user input. | |
function generate() { | |
# Sets the following variables globally: namespace | |
namespace=$(sanitizeNamespace "$name") | |
echo "Generating new datapack project in $parent_dir..." | |
mkdir -p "$parent_dir" | |
cd "$parent_dir" || handleError 2 | |
mkdir "$name" || handleError 3 | |
cd "./$name" || handleError 3 | |
# Inside main project directory: | |
generatePackMeta "$pack_format" "$description" > pack.mcmeta | |
mkdir -p "data/$namespace" | |
cd "./data/$namespace" || handleError 4 | |
if [[ "$pack_format" -ge 43 ]]; then | |
setSubDirsNew | |
else | |
setSubDirsOld | |
fi | |
for subdirectory in "${subdirectories[@]}"; do | |
mkdir -p "$subdirectory" | |
if [[ $subdirectory == "function" ]]; then | |
cd "function" || handleError 4 | |
generateMainFunction > "main.mcfunction" | |
generateLoadFunction > "load.mcfunction" | |
cd "$parent_dir/$name/data" || handleError 4 | |
mkdir -p "minecraft/tags/function" | |
cd "./minecraft/tags/function" || handleError 4 | |
generateLoadTag "$namespace" > "load.json" | |
generateTickTag "$namespace" > "tick.json" | |
cd "$parent_dir/$name/data/$namespace" || handleError 4 | |
elif [[ $subdirectory == "functions" ]]; then | |
cd "functions" || handleError 4 | |
generateMainFunction > "main.mcfunction" | |
generateLoadFunction > "load.mcfunction" | |
cd "$parent_dir/$name/data" || handleError 4 | |
mkdir -p "minecraft/tags/functions" | |
cd "./minecraft/tags/functions" || handleError 4 | |
generateLoadTag "$namespace" > "load.json" | |
generateTickTag "$namespace" > "tick.json" | |
cd "$parent_dir/$name/data/$namespace" || handleError 4 | |
fi | |
done | |
} | |
# Function to clean up any potentially created directories or files before exiting. | |
function cleanUp() { | |
echo "Cleaning up..." | |
local user_home_dir="$HOME" # User's home directory | |
# Only proceed with removal if the created directory is within the user's home directory to limit potential risks. | |
if [[ -n "$parent_dir" && -n "$name" && "$parent_dir" == "$user_home_dir"* ]]; then | |
if [ -d "$parent_dir/$name" ]; then | |
rm -rf "${parent_dir:?}/$name" # Safely remove the created directory within user's home. | |
fi | |
else | |
echo "Cleanup is only allowed within your home directory or its subdirectories. Operating outside of these, requires you to do manual clean-up." | |
echo "The generated files (if any are present) should be in $parent_dir." | |
fi | |
} | |
# Function to handle cleanup and exit on script interruption. | |
function onInterrupt() { | |
cleanUp | |
echo "Exiting..." | |
exit 1 | |
} | |
# Function to handle errors during the datapack generation process. | |
function handleError() { | |
# Something happened! | |
local error_code="$1" | |
case "$error_code" in | |
2) | |
echo "Error: Invalid parent directory! Please provide a valid path." | |
;; | |
3) | |
echo "Error: Failed to create the datapack directory. Is the parent directory writable?" | |
;; | |
4) | |
echo "Error: Failed to create the necessary files and directories for the datapack." | |
;; | |
5) | |
echo "Error: Failed to create the directory with the namespace \"$namespace\"." | |
;; | |
*) | |
echo "Unknown error occurred with error code: $error_code" | |
;; | |
esac | |
cleanUp | |
exit "$error_code" | |
} | |
function main() { | |
detectGum | |
gatherAndConfirm | |
generate | |
echo "Done!" | |
} | |
# Execution starts here: | |
trap onInterrupt SIGINT | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Notes:
echo
in thecleanUp
function, if you're uncomfortable withrm -rf
How to use:
./scripts
.chmod +x /path/to/gen_empty_pack.sh
/path/to/gen_empty_pack.sh