Skip to content

Instantly share code, notes, and snippets.

@Europia79
Created October 18, 2023 08:44
Show Gist options
  • Save Europia79/f02de0cc92bb1673f4f9d0fd3176ba5d to your computer and use it in GitHub Desktop.
Save Europia79/f02de0cc92bb1673f4f9d0fd3176ba5d to your computer and use it in GitHub Desktop.
Save Headers for Sega Mega Drive ; Genesis ; 32x
#!/bin/bash
# _save_headers.sh by Europia79
##########################################
# README #
##########################################
# (1) This script requires Linux, Mac, or Git-for-Windows:
# https://gitforwindows.org/
# https://git-scm.com/download/win
# (2) Requires a ROM set:
# https://r-roms.github.io/
# (3) Requires Megadrive VGM Player v3.30:
# http://www.mjsstuf.x10host.com/pages/vgmPlay/vgmPlay.htm
# https://github.com/TheDeadFish/vgmPlay-vgmConv
# https://github.com/Europia79/vgmPlay-vgmConv
# (4) Requires 7-zip:
# https://www.7-zip.org/download.html
# (5) Add the 7-zip directory to your PATH environment variable:
# https://gist.github.com/nex3/c395b2f8fd4b02068be37c961301caa7
# (6) Alternatively, if you cannot change your PATH environment, then
# you can instead, copy 7-zip to your ~/Git/usr/bin/ folder.
# (7) Edit CONFIG below for Roms Folder & Headers Directory:
##########################################
# CONFIG #
##########################################
ROMS="_roms"
HEADERS="_headers"
BAD_HEADERS="${HEADERS}/_BAD_HEADERS"
temp="temp_abc123xyz789"
##########################################
# CONSTANTS & GLOBALS #
##########################################
# add line numbers in debug mode (bash set -x ./_save_headers.sh):
PS4='+ \e[0;33m${LINENO}\e[0m::'
# 7z??'.="37 7A BC AF 27 1C"
CMAGIC_7Z="377ABCAF271C"
# PK..="50 4B 03 04"
CMAGIC_ZIP="504B0304"
global_args=("$@")
declare -i arg_count_nes_cli=0
declare -a CLEANUP_FILES=()
declare -a ADDED_FILES=()
declare -a FAILED_FILES=()
declare -a UNSUPPORTED=()
# grep -n "^function " _save_headers.sh | sed 's/^/# /g' | sed 's/:function /::/g' | sed 's/() {/()::/g'
##########################################
# FUNCTION MENU: (Ctl+G) #
##########################################
# 89::pause()::
# 94::close()::
# 103::close_now()::
# 109::close_delay()::
# 114::pipeout()::
# 119::die()::
# 123::die_now()::
# 128::die_delay()::
# 133::cleanupValidation()::
# 152::cleanup()::
# 159::onExit()::
# 170::get_bytesize()::
# 174::get_magic_num()::
# 177::is_archive()::
# 180::has_archive_magic()::
# 183::has_7z_magic()::
# 186::has_zip_magic()::
# 189::is_archive_file()::
# 192::is_7z()::
# 195::is_zip()::
# 198::is_gen()::
# 201::is_32x()::
# 204::is_smd()::
# 208::has_gen_ext()::
# 221::parseInput()::
# 234::parseFullpath()::
# 259::getFile()::
# 266::getDirectory()::
# 273::getBase()::
# 280::getExtention()::
# 289::isFileType()::
# 304::getFileType()::
# 317::windows_fix()::
# 339::getFileType_ReturnValues_Comment()::
# 394::FILETYPE_CONSTANTS()::
# 459::require_args()::
# 462::onStartupClientsideValidation()::
##########################################
# CONTROL FUNCTIONS #
##########################################
# PAUSE
function pause() {
read -rsn1 -p "Press any key to continue..."
echo ""
}
# CLOSE
function close() {
if [ $# -eq 0 ]
then
close_now $?
else
close_now $1
fi
}
# CLOSE_NOW
function close_now() {
local status=0
if [ -n "$1" ] && (( $1 + 0 > 0 )); then status=$1; fi
exit $status
}
# CLOSE_DELAY
function close_delay() {
pause
close $1
}
# PIPEOUT
function pipeout() {
printf "%s" $*
close 0
}
# DIE
function die() {
die_now "$1"
}
# DIE_NOW
function die_now() {
printf "Aborted: %s\n" "$"; echo ""
close 1
}
# DIE_DELAY
function die_delay() {
printf "Aborted: %s\n" "$1"; echo ""
pause
close 1
}
function cleanupValidation() {
printf "\e[1;36m"
for failedfile in "${FAILED_FILES[@]}"
do
echo "FAILED: ${failedfile}"
done
for ufile in "${UNSUPPORTED[@]}"
do
echo "UNSUPPORTED: ${ufile}"
done
echo "TOTAL HEADERS: "$(find "./${HEADERS}" -type f | wc -l)
echo "ADDED: ${#ADDED_FILES[@]}"
echo "BAD: ${#FAILED_FILES[@]}"
echo "UNSUPPORTED: ${#UNSUPPORTED[@]}"
pwd
printf "\e[0m"
}
# Remove specific files before dying onError/onExit.
function cleanup() {
rm -rf -- "${temp}"
for cfile in "$@"
do
rm -vf -- "${cfile}"
done
}
function onExit() {
printf "\e[1;33m%s\e[1;31m\n" "onExit() cleanup..." >&2
cleanup "${CLEANUP_FILES[@]}"
printf "\e[1;33m%s\e[0m\n" "...done." >&2
cleanupValidation
pause
} >&2
trap onExit EXIT
##########################################
# FILE METHODS #
##########################################
function get_bytesize() {
wc -c < "$1"
}
# return_val=$(get_magic_num "${file1}")
function get_magic_num() {
xxd -p -l 4 "$1" | tr '[:lower:]' '[:upper:]'
}
function is_archive() {
$(is_archive_file "$1") || $(has_archive_magic "$1")
}
function has_archive_magic() {
$(has_zip_magic "$1") || $(has_7z_magic "$1")
}
function has_7z_magic() {
[[ ${CMAGIC_7Z} = $(get_magic_num "$1") ]]
}
function has_zip_magic() {
[[ ${CMAGIC_ZIP} = $(get_magic_num "$1") ]]
}
function is_archive_file() {
$(is_7z "$1") || $(is_zip "$1")
}
function is_7z() {
isFileType "$1" "7z"
}
function is_zip() {
isFileType "$1" "zip"
}
function is_gen() {
isFileType "$1" "gen"
}
function is_32x() {
isFileType "$1" "32x"
}
function is_smd() {
isFileType "$1" "smd"
}
# 32x 68k bin gen md smd
function has_gen_ext() {
ext=$(getExtention "$1")
case "${ext}" in
32x) true; return;;
68k) true; return;;
bin) true; return;;
gen) true; return;;
md) true; return;;
smd) true; return;;
*) false; return;;
esac
}
# Null Check for function arguments: $* $1 $2 etc.
function parseInput() {
local PARSED_INPUT=""
if [ -n "$1" ]
then
PARSED_INPUT="$1"
else
die "$(basename $0): function argument cannot be NULL"
fi
echo "${PARSED_INPUT}"
}
# $(parseFullpath _RETURN_VAR "${__INPUT_VAL}")
# _RETURN_VAR: $1 = A named reference to the array used to store the return values.
# __INPUT_VAL: $2 = the input string.
function parseFullpath() {
local -n returnvar=$1
local input_path=$(parseInput "$2")
# Convert backslashes on \Windows\ to foward slashes: /Windows/
local FULLPATH="${input_path//\\//}"
# [0] Strip longest match of */ from start
local file="${FULLPATH##*/}"
# [1] Substring from 0 thru pos of filename
local dir="${FULLPATH:0:${#FULLPATH} - ${#file}}"
[ -z "${dir}" ] && dir="."
# [2] Strip shortest match of . plus at least one non-dot char from end
local base="${file%.[^.]*}"
# [3] If we have an extension and no base, it's really the base
local ext="${file:${#base} + 1}"
if [ -z "${base}" ] && [ -n "${ext}" ]
then
base=".${ext}"
ext=""
fi
returnvar[0]="${file}"
returnvar[1]="${dir}"
returnvar[2]="${base}"
returnvar[3]="${ext}"
}
# gets the entire Filename including extention.
function getFile() {
local -a path_array=()
local file_input=$(parseInput "$1")
parseFullpath path_array "${file_input}"
echo "${path_array[0]}"
}
# gets the Directory, or, "." if none.
function getDirectory() {
local -a path_array=()
local dir_input=$(parseInput "$1")
parseFullpath path_array "${dir_input}"
echo "${path_array[1]}"
}
# get only the base Filename without the extention.
function getBase() {
local -a path_array=()
local base_input=$(parseInput "$1")
parseFullpath path_array "${base_input}"
echo "${path_array[2]}"
}
# gets the Extention without the period.
function getExtention() {
local -a path_array=()
local ext_input=$(parseInput "$1")
parseFullpath path_array "${ext_input}"
echo "${path_array[3]}"
}
# Remember, type 'nes' specifically refers to an NES ROM with a header !!!
# $(isFileType "$1" bps) && echo "BPS=true" || echo "BPS=false"
# $(isFileType "$2" nes) && echo "NES=true" || echo "NES=false"
function isFileType() {
local T1=$(getFileType "$1")
local T2="${qFILE_TYPE[$2]}"
[[ ${T1} = ${T2}* ]] && return
if [[ ${T2} = "declare -a"* ]]
then
eval "${T2}"
for element in "${array[@]}"
do
[[ ${T1} = ${element}* ]] && return
done
fi
false
}
# Fully qualified FileType with extraneous information:
function getFileType() {
if [ ! -f "$1" ]; then die "getFileTypeOf() requires a File argument: Not: $1"; fi
file -b "$1"
}
##########################################
# WINDOWS BUG FIX #
##########################################
# Fixes a BUG on Windows:
# Where Drag N Drop changes the working directory to
# C:\Windows\system32
# /c/Windows/system32
# This functions attempts to change it back based on the input of DragNdrop:
# changes=$(windows_fix)
function windows_fix() {
echo "windows_fix()"
local THIS_DIR=$(pwd)
for arg in "${global_args[@]}"
do
if [ "/c/Windows/system32" = "${THIS_DIR}" ]
then
local temp=$(getDirectory "${arg}")
if [ -n "${arg}" ] && [ -n "${temp}" ] && [ -d "${temp}" ]
then
echo "CHANGING DIRECTORY TO..."
echo "${temp}"
THIS_DIR="${temp}"
cd "${THIS_DIR}"
pwd
return 1
fi
fi
done
return 0
}
# Comment: do-nothing.
function getFileType_ReturnValues_Comment() {
true
}
##########################################
# getFileType() return values #
##########################################
#-----------------------------------------
# input: Output
#-----------COMPRESSED-FILES--------------
# .7z 7-zip archive data, version 0.4
# .zip Zip archive data, at least v2.0 to extract, compression method=deflate
#-----------PATCH-FILES-------------------
# .aps
# .bps BPS patch file
# .ebp IPS patch file
# .ips IPS patch file
# .ppf Playstation Patch File version 3.0, PPF 3.0 patch, Imagetype BIN (any), Blockcheck disabled, Undo data not available, description: FFT: WotL - Valeria 2.2 (PSP USA)
# .rup data
# .ups UPS patch file
# .xdelta VCDIFF binary diff
#-----------ROM-FILES---------------------
# .pce data
# .nes NES ROM image (iNES): 8x16k PRG, 0x8k CHR [H-mirror] [SRAM]
# .fds Famicom Disk System disk image: FMC-ARM, mfr FFFFFFA4 (Rev.00) (2 sides)
# .unh data
# .unf NES ROM image (UNIF v7 format)
# .bs data
# .st Sufami Turbo ROM image: "\276\260\327\2", ID 010003, series index 1 [FastROM]
# .sfc data
# .n64 Nintendo 64 ROM image (32-bit byteswapped)
# .v64 Nintendo 64 ROM image (V64)
# .z64 Nintendo 64 ROM image: "SUPER MARIO 64 " (NSME, Rev.00)
# .iso Nintendo GameCube disc image: "The Legend of Zelda Twilight Princess" (GZ2E01, Rev.00)
# .iso Nintendo Wii disc image: "SPORTS PACK for REVOLUTION" (RSPE01, Rev.01)
# .wbfs Nintendo Wii disc image (WBFS format): "SPORTS PACK for REVOLUTION" (RSPE01, Rev.01)
# .gb Game Boy ROM image: "ZELDA" (Rev.02) [MBC1+RAM+BATT], ROM: 4Mbit, RAM: 64Kbit
# .gbc Game Boy ROM image: "PM_CRYSTAL" (Rev.01) [CGB ONLY] [MBC3+TIMER+RAM+BATT], ROM: 16Mbit, RAM: 256Kbit
# .gba Game Boy Advance ROM image: "POKEMON RUBY" (AXVE01, Rev.02)
# .nds Nintendo DS ROM image: "CASTLEVANIA1" (ACVPA4, Rev.00) (decrypted)
# .gg Sega Game Gear ROM image: 2408 (Rev.01) (256 KB)
# .sms Sega Master System ROM image: 7076 (Rev.00) (256 KB)
# .32x Sega 32X ROM image: "DOOM " (GM MK-84506-00, (C)SEGA 1994.OCT)
# .68k Sega Mega Drive / Genesis ROM image: "\377\377\377\377" (\377\377\377\3, \377\377\377\377)
# .bin Sega Mega Drive / Genesis ROM image: "SONIC THE " (GM 00001051-02, (C)SEGA 1992.SEP)
# .gen Sega Mega Drive / Genesis ROM image: "NHL HOCKEY " (GM T-50236 -00, (C)T-50 1991.MAY)
# .md Sega Mega Drive / Genesis ROM image: "EA HOCKEY " (GM T-50236 -50, (C)T-50 1991.JUN)
# .smd Sega Mega Drive / Genesis ROM image (SMD format): 64x16k blocks, last in series or standalone
# .img Sega Mega CD disc image: "SOULSTAR " (GM T-115035-00, (C)T-1151994.NOV), 2352-byte sectors
# .iso Sega Mega CD disc image: "SOULSTAR " (GM T-115035-00, (C)T-1151994.NOV), 2048-byte sectors
# .bin Sega Saturn disc image: "AREA 51 " (T-9705H , V1.000) (2352-byte sectors)
# .bin Sega Dreamcast disc image: "CRAZY TAXI " (MK-51035 , V1.004) (2352-byte sectors)
# .iso UDF filesystem data (version 1.5) 'FINAL_FANTASY_X'
##########################################
# CONSTANTS2 #
##########################################
function FILETYPE_CONSTANTS() {
true
}
declare -A qFILE_TYPE
declare -A qFILE_TYPE_COM
declare -A qFILE_TYPE_DIF
declare -A qFILE_TYPE_ROM
qFILE_TYPE_COM["7z"]="7-zip archive"
qFILE_TYPE_COM[zip]="Zip archive"
qFILE_TYPE_DIF[aps]="UNKNOWN: NOT SUPPORTED"
qFILE_TYPE_DIF[bps]="BPS patch file"
qFILE_TYPE_DIF[ebp]="IPS patch file"
qFILE_TYPE_DIF[ips]="IPS patch file"
qFILE_TYPE_DIF[ppf]="Playstation Patch File"
qFILE_TYPE_DIF[rup]="data"
qFILE_TYPE_DIF[ups]="UPS patch file"
qFILE_TYPE_DIF[xdelta]="VCDIFF binary diff"
qFILE_TYPE_ROM[pce]="data"
qFILE_TYPE_ROM[nes]="NES ROM image (iNES)"
qFILE_TYPE_ROM[fds]="Famicom Disk System disk image:"
qFILE_TYPE_ROM[unh]="data"
qFILE_TYPE_ROM[unf]="NES ROM image (UNIF"
qFILE_TYPE_ROM[bs]="data"
qFILE_TYPE_ROM[st]="Sufami Turbo ROM image:"
qFILE_TYPE_ROM[sfc]="data"
qFILE_TYPE_ROM[n64]="Nintendo 64 ROM image (32-bit byteswapped)"
qFILE_TYPE_ROM[v64]="Nintendo 64 ROM image (V64)"
qFILE_TYPE_ROM[z64]="Nintendo 64 ROM image:"
qFILE_TYPE_ROM[wbfs]="Nintendo Wii disc image (WBFS format):"
qFILE_TYPE_ROM[gb]="Game Boy ROM image:"
qFILE_TYPE_ROM[gbc]="Game Boy ROM image:"
qFILE_TYPE_ROM[gba]="Game Boy Advance ROM image:"
qFILE_TYPE_ROM[nds]="Nintendo DS ROM image:"
qFILE_TYPE_ROM[gg]="Sega Game Gear ROM image:"
qFILE_TYPE_ROM[sms]="Sega Master System ROM image:"
qFILE_TYPE_ROM["32x"]="Sega 32X ROM image:"
qFILE_TYPE_ROM["68k"]="Sega Mega Drive / Genesis ROM image:"
qFILE_TYPE_ROM[gen]="Sega Mega Drive / Genesis ROM image:"
qFILE_TYPE_ROM[md]="Sega Mega Drive / Genesis ROM image:"
qFILE_TYPE_ROM[smd]="Sega Mega Drive / Genesis ROM image (SMD format):"
qFILE_TYPE_ROM[img]="Sega Mega CD disc image:"
declare -a array=("Sega Mega Drive / Genesis ROM image:" "Sega Saturn disc image:" "Sega Dreamcast disc image:")
qFILE_TYPE_ROM[bin]=$(declare -p array)
declare -a array=("Nintendo GameCube disc image:" "Nintendo Wii disc image:" "Sega Mega CD disc image:" "UDF filesystem data")
qFILE_TYPE_ROM[iso]=$(declare -p array)
array=()
# re-populate array Object:
# if [ "declare -a" = "${qFILE_TYPE[$key]::10}" ]; then eval "${qFILE_TYPE[$key]}"; fi
# or
# if [[ $value = "declare -a"* ]]; then eval "$value"; ...; fi
for key in "${!qFILE_TYPE_COM[@]}"
do
qFILE_TYPE[$key]="${qFILE_TYPE_COM[$key]}"
done
for key in "${!qFILE_TYPE_DIF[@]}"
do
qFILE_TYPE[$key]="${qFILE_TYPE_DIF[$key]}"
done
for key in "${!qFILE_TYPE_ROM[@]}"
do
qFILE_TYPE[$key]="${qFILE_TYPE_ROM[$key]}"
done
##########################################
# CLIENT-SIDE METHODS #
##########################################
function require_args() {
if [ $arg_count_nes_cli -le $(( $1 - 1 )) ]; then die "Not enough command line arguments (${arg_count_nes_cli}): ${operation}() requires $1"; fi
}
function onStartupClientsideValidation() {
# command -v xxd
true
}
##########################################
# START #
##########################################
onStartupClientsideValidation
mkdir -p "${HEADERS}"
mkdir -p "${BAD_HEADERS}"
cd "${ROMS}"
total_files=$(find . -type f | wc -l)
declare -i index=0
for file in *
do
index=$((index + 1))
printf "_progress: %d/%d\r" "${index}" "${total_files}"
mkdir -p "${temp}"
#----------------------------------------------
if is_archive "${file}"
then
if is_zip "${file}"
then
unzip "${file}" -d "${temp}"
cd "${temp}"
elif is_7z "${file}"
then
7z e "${file}" -o"${temp}" >/dev/null
cd "${temp}"
fi
else
cp -f -- "${file}" "${temp}/${file}"
cd "${temp}"
fi
#----------------------------------------------
for tfile in *
do
if is_smd "${tfile}"
then
# NOT SUPPORTED
FAILED_FILES+=("${tfile}")
xxd -ps -l 0x200 "${tfile}" | xxd -r -ps > "../../${BAD_HEADERS}/${tfile}"
elif is_gen "${tfile}" || is_32x "${tfile}"
then
xxd -ps -l 0x200 "${tfile}" | xxd -r -ps > "../../${HEADERS}/${tfile}"
ADDED_FILES+=("${tfile}")
elif has_gen_ext "${tfile}"
then
FAILED_FILES+=("${tfile}")
xxd -ps -l 0x200 "${tfile}" | xxd -r -ps > "../../${BAD_HEADERS}/${tfile}"
else
# NOT SUPPORTED
UNSUPPORTED+=("${tfile}")
fi
done
cd ..
rm -rf -- "${temp}"
done
cd ..
pause
close
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment