Skip to content

Instantly share code, notes, and snippets.

@Europia79
Last active September 11, 2023 19:41
Show Gist options
  • Save Europia79/8840f6244f17bcd0300696917596e648 to your computer and use it in GitHub Desktop.
Save Europia79/8840f6244f17bcd0300696917596e648 to your computer and use it in GitHub Desktop.
#!/bin/bash
# NES Flips Wrapper by Europia79
##########################################
# README #
##########################################
# (1) This script requires Linux, Mac, or Git-for-Windows:
# https://gitforwindows.org/
# https://git-scm.com/download/win
# (2) This script also requires Floating IPS (flips) v1.31:
# https://github.com/Alcaro/Flips
# https://www.romhacking.net/utilities/1040/
# https://www.smwcentral.net/?a=details&id=11474&p=section
# (3) Add the flips directory to your PATH environment variable:
# https://gist.github.com/nex3/c395b2f8fd4b02068be37c961301caa7
# (4) Alternatively, if you cannot change your PATH environment, then
# you can instead, copy flips to your ~/Git/usr/bin/ folder.
##########################################
# CONSTANTS1 #
##########################################
# add line numbers in debug mode (bash set -x ./nes.sh):
PS4='+ \e[0;33m${LINENO}\e[0m::'
# echo "${D2B[$(( x & 0x80 ))]}"
D2B=({0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1}{0..1})
# 7z??'.="37 7A BC AF 27 1C"
CMAGIC_7Z="377ABCAF271C"
# PK..="50 4B 03 04"
CMAGIC_ZIP="504B0304"
# BPS1="42 50 53 31"
PMAGIC_BPS="42505331"
# UPS1="55 50 53 31"
PMAGIC_UPS="55505331"
# XDELTA="D6 C3 C4 00"
PMAGIC_XDELTA="D6C3C400"
# NINJA2="4E 49 4E 4A 41 32"
PMAGIC_RUP="4E494E4A4132"
# NES.="4E 45 53 1A"
RMAGIC_NES="4E45531A"
# FDS.="46 44 53 1A"
RMAGIC_FDS="4644531A"
# UNIF="55 4E 49 46"
RMAGIC_UNF="554E4946"
global_args=("$@")
declare -i arg_count_nes_cli=0
declare -a CLEANUP_FILES=()
# grep -n "^function " nes.sh | sed 's/^/# /g' | sed 's/:function /::/g' | sed 's/() {/()::/g'
##########################################
# FUNCTION MENU: (Ctl+G) #
##########################################
# 138::pause()::
# 143::close()::
# 152::close_now()::
# 158::close_delay()::
# 163::pipeout()::
# 168::die()::
# 172::die_now()::
# 177::die_delay()::
# 182::cleanupValidation()::
# 189::cleanup()::
# 195::onExit()::
# 205::displayFunctionMenu()::
# 209::displayVersion()::
# 222::displayHelp()::
# 276::displayCompressionSupport()::
# 286::displayPatchSupport()::
# 295::displayRomSupport()::
# 314::showAllFileTypes()::
# 336::ERROR_INVALID_FILE_FOR()::
# 349::ERROR_INVALID_BPS()::
# 353::ERROR_INVALID_NES()::
# 357::ERROR_INVALID_FDS()::
# 360::get_bytesize()::
# 363::is_16bytes()::
# 366::has_rom_header()::
# 369::is_rom_header()::
# 373::get_magic_num()::
# 376::has_zip_magic()::
# 379::has_bps_magic()::
# 382::has_nes_magic()::
# 385::has_fds_magic()::
# 388::has_unf_magic()::
# 391::has_tmp_ext()::
# 394::has_bps_ext()::
# 397::has_nes_ext()::
# 400::has_fds_ext()::
# 403::has_unf_ext()::
# 406::is_zip()::
# 409::is_bps()::
# 413::is_nes()::
# 416::is_fds()::
# 419::is_unf()::
# 422::extract_bps_crc32()::
# 425::extract_ups_crc32()::
# 428::extract_zip_crc32()::
# 431::calculate_crc32()::
# 434::get_crc32()::
# 443::get_nes_hashes()::
# 451::get_nes_hash()::
# 456::get_hash()::
# 459::get_all_hashes()::
# 468::parseInput()::
# 481::parseFullpath()::
# 506::getFile()::
# 513::getDirectory()::
# 520::getBase()::
# 527::getExtention()::
# 534::getFileTypeExt()::
# 581::isFileType()::
# 596::getFileType()::
# 610::encode_header()::
# 646::decode_header()::
# 677::get_bps_header()::
# 684::get_nes_header_hex()::
# 696::get_nes_header_binary()::
# 702::get_header()::
# 715::save_romheader_tofile()::
# 744::get_temp_no_header()::
# 764::echo_header()::
# 780::print_header()::
# 784::echo_manifest()::
# 797::print_manifest()::
# 800::has_manifest()::
# 803::save_manifest()::
# 815::insert_manifest()::
# 820::insert_header()::
# 823::insert_from()::
# 828::insert()::
# 839::apply_patch()::
# 878::create_patch()::
# 911::windows_fix()::
# 933::getFileType_ReturnValues_Comment()::
# 988::FILETYPE_CONSTANTS()::
# 1094::require_args()::
# 1097::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"
find . -type f -name "*.tmp" -printf "WARNING: Not removed: %p\n"
find . -type f -name "header*" -printf "WARNING: Not removed: %p\n"
printf "\e[0m"
}
# Remove specific files before dying onError/onExit.
function cleanup() {
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
} >&2
trap onExit EXIT
##########################################
# DISPLAY METHODS #
##########################################
function displayFunctionMenu() {
grep -n "^function " nes.sh | sed 's/^/# /g' | sed 's/:function /::/g' | sed 's/() {/()::/g'
}
# displayVersion
function displayVersion() {
cat << VERSION_EOF
Flips NES Wrapper: (nes.sh): by Europia79
./nes.sh revision: r1.31
for
----flips version: v1.31
You have Floating IPS (flips) version:
VERSION_EOF
flips --version
}
# displayHelp
function displayHelp() {
cat << HELP_EOF
Allows the use of a BPS manifest to save & restore NES headers:
Which allows patches to automatically & seemlessly work with ANY NES ROM.
Usage:
./nes.sh [options] [--] <parameters>
Options:
-a, --apply apply patch (w/ automatic NES header detection)
-c, --create create patch (w/ automatic NES header detection)
-d, --has-header
-e, --echo-header echo entire BPS Header in DEBUG format
-f, --echo-manifest echo only BPS manifest metadata as Hex
-g, --get-filetype print filetype for given File
-i, --is-filetype compare filetype for File with given type
-j, --get-alltypes print all supported filetypes for Patches, ROMs, & Compression
-n, --insert-manifest insert manifest (as header) into a ROM
-p, --print-manifest print manifest from patch file
-q, --has-manifest
-r, --print-header decode & print BPS patch file header
-s, --save-manifest save manifest to file [header.nes]
-t, --get-temp given Rom.nes, creates Rom.tmp without a header
-x, --get-hash extract CRC32 from .bps OR calculate for file
-z, --get-all-hashes prints all hash info for every every file given
-h, --help display this help
-u, --menu display all functions & methods
-v, --version display the version
Example: Create Patch:
./nes.sh -c Baserom.nes Hack.nes Patch.bps
--Detects NES Header & saves it into the Patch.bps manifest.
Example: Apply Patch:
./nes.sh -a Patch.bps Baserom.nes Hack.nes
--Automatically detects manifest in Patch.bps.
--Automatically & temporarily removes header from Baserom.nes (if necessary).
--Automatically applies header (from manifest) to Hack.nes.
Other Examples:
./nes.sh -e Patch.bps
./nes.sh -p Patch.bps
./nes.sh -s Patch.bps <header.nes>
./nes.sh -i 'Baserom.ext' nes && echo true || echo false
Warning:
FileType 'nes' refers specifically to Headered NES ROMs (iNes1/iNes2).
FileType 'nes' does not refer to '.unh' or '.unf': See option -j for more info.
HELP_EOF
}
# DISPLAY FORMAT
dformat=".%-10s %s\n"
# DISPLAY COMPRESSION SUPPORT
function displayCompressionSupport() {
local -a keys=( $( echo ${!qFILE_TYPE_COM[@]} | tr ' ' $'\n' | sort ) )
for key in "${keys[@]}"
do
local value="${qFILE_TYPE_COM[$key]}"
# shellcheck disable=SC2059
printf "$dformat" "$key" "$value"
done
}
# DISPLAY PATCH SUPPORT
function displayPatchSupport() {
local -a keys=( $( echo ${!qFILE_TYPE_DIF[@]} | tr ' ' $'\n' | sort ) )
for key in "${keys[@]}"
do
local value="${qFILE_TYPE_DIF[$key]}"
printf "$dformat" "$key" "$value"
done
}
# DISPLAY ROM SUPPORT
function displayRomSupport() {
local -a keys=( $( echo ${!qFILE_TYPE_ROM[@]} | tr ' ' $'\n' | sort ) )
for key in "${keys[@]}"
do
local value="${qFILE_TYPE_ROM[$key]}"
if [[ $value = "declare -a"* ]]
then
eval "$value"
for k in "${!array[@]}"
do
local v="${array[$k]}"
printf "$dformat" "$key" "$v"
done
else
printf "$dformat" "$key" "$value"
fi
done
}
# DISPLAY ALL SUPPORTED FILETYPES
function showAllFileTypes() {
cat << TABLE_HEADER
##########################################
# -g, --get-filetype #
##########################################
#-----------------------------------------
file: type:
TABLE_HEADER
cat << COM_SUPPORT
#------------COMPRESSED-FILES-------------
COM_SUPPORT
displayCompressionSupport
cat << DIF_SUPPORT
#--------------PATCH-FILES----------------
DIF_SUPPORT
displayPatchSupport
cat << ROM_SUPPORT
#----------------ROM-FILES----------------
ROM_SUPPORT
displayRomSupport
}
# ERROR_INVALID_FILE_FOR "calling_function" "${filename}" "${actual_header}" "${expected_header}" ".ext"
function ERROR_INVALID_FILE_FOR() {
cat << FILETYPE_ERROR
'$1():' Invalid $5 file with unknown header:
---------Filename: $2
------File Header: $3
--Expected Header: $4
FILETYPE_ERROR
}
##########################################
# FILE METHODS #
##########################################
# ERROR_INVALID_BPS "calling_function" "${bps_file}" "${bps_header}"
function ERROR_INVALID_BPS() {
ERROR_INVALID_FILE_FOR "$1" "$2" "$3" "${PMAGIC_BPS}" ".bps"
}
# ERROR_INVALID_NES "calling_function" "${nes_file}" "${nes_header}"
function ERROR_INVALID_NES() {
ERROR_INVALID_FILE_FOR "$1" "$2" "$3" "${RMAGIC_NES}" ".nes"
}
# ERROR_INVALID_FDS "calling_function" "${fds_file}" "${fds_header}"
function ERROR_INVALID_FDS() {
ERROR_INVALID_FILE_FOR "$1" "$2" "$3" "${RMAGIC_FDS}" ".fds"
}
function get_bytesize() {
wc -c < "$1"
}
function is_16bytes() {
(( $(get_bytesize "$1")+0 == 16 ))
}
function has_rom_header() {
has_nes_magic "$1" || has_fds_magic "$1"
}
function is_rom_header() {
has_rom_header "$1" && is_16bytes "$1"
}
# return_val=$(get_magic_num "${file1}")
function get_magic_num() {
xxd -p -l 4 "$1" | tr '[:lower:]' '[:upper:]'
}
function has_zip_magic() {
[[ ${CMAGIC_ZIP} = $(get_magic_num "$1") ]]
}
function has_bps_magic() {
[[ ${PMAGIC_BPS} = $(get_magic_num "$1") ]]
}
function has_nes_magic() {
[[ ${RMAGIC_NES} = $(get_magic_num "$1") ]]
}
function has_fds_magic() {
[[ ${RMAGIC_FDS} = $(get_magic_num "$1") ]]
}
function has_unf_magic() {
[[ ${RMAGIC_UNF} = $(get_magic_num "$1") ]]
}
function has_tmp_ext() {
[[ "tmp" = $(getExtention "$1") ]]
}
function has_bps_ext() {
[[ "bps" = $(getExtention "$1") ]]
}
function has_nes_ext() {
[[ "nes" = $(getExtention "$1") ]]
}
function has_fds_ext() {
[[ "fds" = $(getExtention "$1") ]]
}
function has_unf_ext() {
[[ "unf" = $(getExtention "$1") ]]
}
function is_zip() {
has_zip_magic "$1"
}
function is_bps() {
has_bps_magic "$1"
}
# Only worry about .nes roms that are actually HEADERED.
function is_nes() {
has_nes_magic "$1"
}
function is_fds() {
has_fds_magic "$1"
}
function is_unf() {
has_unf_magic "$1"
}
function extract_bps_crc32() {
tail -c12 "$1" | xxd -ps -l 4
}
function extract_ups_crc32() {
tail -c12 "$1" | xxd -ps -l 4
}
function extract_zip_crc32() {
xxd -ps -s 14 -l 4 "$1"
}
function calculate_crc32() {
cat "$1" | gzip -1 -c | tail -c8 | xxd -ps -l 4
}
function get_crc32() {
magic_num=$(get_magic_num "$1")
case ${magic_num} in
${PMAGIC_BPS}) extract_bps_crc32 "$1";;
${PMAGIC_UPS}) extract_ups_crc32 "$1";;
${CMAGIC_ZIP}) extract_zip_crc32 "$1";;
*) calculate_crc32 "$1";;
esac
}
function get_nes_hashes() {
local -a hashes=()
hashes[0]=$(calculate_crc32 "$1")
temp=$(get_temp_no_header "$1")
hashes[1]=$(calculate_crc32 "${temp}")
rm -f -- "${temp}"
declare -p hashes
}
function get_nes_hash() {
local return_val=$(get_nes_hashes "$1")
eval "${return_val}"
echo "${hashes[1]}"
}
function get_hash() {
get_crc32 "$1"
}
function get_all_hashes() {
local -n hash_array=$1
for i in "${!files[@]}"
do
local hash=$(get_hash "${files[$i]}")
hash_array[$i]="${hash}"
done
}
# 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]}"
}
# Requires a File parameter, but allows a String arg for Testing purposes.
function getFileTypeExt() {
local T=""
if [ -f "$1" ]
then
T=$(getFileType "$1")
else
T="$1"
fi
local extension=""
if [[ $T = data ]]
then
extension=$(getExtention "$1")
echo "${extension}"
return 0
fi
for key in "${!qFILE_TYPE[@]}"
do
local val="${qFILE_TYPE[$key]}"
if [[ ${val} = "declare -a"* ]]
then
# re-construct array Object:
eval "${qFILE_TYPE[$key]}"
for k in "${!array[@]}"
do
val="${array[$k]}"
if [[ $T = ${val}* ]]
then
extension="$key"
echo "$extension"
return 0
fi
done
else
if [[ $T = ${val}* ]]
then
extension="$key"
echo "$extension"
return 0
fi
fi
done
extension=$(getExtention "$1")
echo "$extension"
}
# 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"
}
##########################################
# NES/BPS METHODS #
##########################################
# -------------------------------------------------------------
# encoded_message=$(encode_header $1 $2 $3)
# $1=${source_size}
# $2=${target_size}
# $3=${mf_size}
# printf "${encoded_message}" | xxd -r -p > bps_header.bin
# -------------------------------------------------------------
function encode_header() {
local string_data="${PMAGIC_BPS}"
declare -a encode_array
read -a encode_array <<< $*
for value in "${encode_array[@]}"
do
local -i data=${value}
while true
do
local -i x=$(( ${data} & 0x7f ))
let "data >>= 7"
if [ ${data} = 0 ]
then
local -i bits=$((0x80 | ${x}))
string_data="${string_data}"$(printf '%02X' "${bits}")
break
fi
string_data="${string_data}"$(printf '%02X' "${x}")
let "data -= 1"
done
done
printf "${string_data}"
# printf "${string_data}" | xxd -r -p > bps_header.bin
}
# -------------------------------------------------------------
# decoded_message=$(decode_header "${patch_file}")
# declare -a header_array
# read -a header_array <<< ${decoded_message}
# magic_num=${header_array[0]}
# source_size=${header_array[1]}
# target_size=${header_array[2]}
# mf_size=${header_array[3]}
# mf_index=${header_array[4]}
# metadata=$(xxd -p -s ${mf_index} -l ${mf_size} ${patch_file})
# printf "${metadata}" | xxd -r -p > header.nes
# -------------------------------------------------------------
function decode_header() {
local -a decoded=()
local bps_file=$1
local -i index=4
local bps_header=$(get_magic_num "${bps_file}")
if [[ ${PMAGIC_BPS} != ${bps_header} ]]
then
die $(ERROR_INVALID_BPS "decode_heaer" "${bps_file}" "${bps_header}")
fi
decoded[0]="${PMAGIC_BPS}"
for return_var in {1..3}
do
local -i data=0
local -i shifter=1
while true
do
local -i x=0x$(xxd -p -s ${index} -l 1 "${bps_file}")
index+=1
data+=$((( x & 0x7f ) * ${shifter} ))
if [ $((x & 0x80)) != 0 ]; then break; fi
shifter=$(( ${shifter} << 7 ))
data=$(( ${data} + ${shifter} ))
done
decoded[${return_var}]=${data}
done
decoded[4]=${index}
echo "${decoded[*]}"
}
# return_message=$(get_bps_header "${bps_file}")
# declare -a header_array
# read -a header_array <<< ${return_message}
function get_bps_header() {
decoded_message=$(decode_header "$1")
declare -a bps_header_array
read -a bps_header_array <<< ${decoded_message}
echo "${bps_header_array[*]}"
}
# return_val=$(get_nes_header_hex "${romfile_patchfile}")
function get_nes_header_hex() {
if is_bps "$1" && has_manifest "$1"
then
echo_manifest "$1"
return
elif has_rom_header "$1"
then
xxd -p -l 16 "$1"
return
fi
echo ""
}
function get_nes_header_binary() {
get_nes_header_hex "$1" | xxd -r -p
}
# return_message=$(get_header "${rom_or_patch_file}")
# declare -a header_array
# read -a header_array <<< ${return_message}
function get_header() {
magic_num=$(get_magic_num "$1")
if is_bps "$1" && has_manifest "$1"
then
get_bps_header "$1"
elif has_rom_header "$1"
then
get_nes_header_hex "$1"
else
echo ""
fi
}
# INPUT: .bps .nes .fds
function save_romheader_tofile() {
if [ -z "$1" ]; then echo ""; return; fi
local temp=""
if (( $# >= 2 ))
then
tmp="$2"
else
temp="header.tmp"
fi
get_nes_header_binary "$1" > "${temp}"
local rom_header_file=""
if has_tmp_ext "${temp}"
then
if is_nes "${temp}"
then
rom_header_file=$(getBase "${temp}")".nes"
elif is_fds "${temp}"
then
rom_header_file=$(getBase "${temp}")".fds"
else
rom_header_file="${temp}"
fi
else
rom_header_file="${temp}"
fi
mv -f -- "${temp}" "${rom_header_file}"
echo "${rom_header_file}"
}
# temp_file=$(get_temp_no_header "${nes_rom}")
function get_temp_no_header() {
local temp=""
if [ -z "$1" ]; then die "get_temp_no_header(): Method Failure !!! Bad arg: $1"; fi
if [ -f "$1" ]
then
temp=$(getBase "$1")
temp+=".tmp"
if has_rom_header "$1"
then
xxd -p -s 16 "$1" | xxd -r -p > "${temp}"
else
cp -f -- "$1" "${temp}"
fi
else
temp=$(getBase "$1")".tmp"
touch "${temp}"
fi
echo ${temp}
}
# Implementation for print_header()
function echo_header() {
decoded_message=$(get_header "$1")
declare -a bps_hdr
read -a bps_hdr <<< ${decoded_message}
echo "File: $1"
local -i mf_index=${bps_hdr[4]}
local -i mf_len=${bps_hdr[3]}
local -i hdr_len=$(( mf_index + mf_len ))
xxd -l 0x$hdr_len "$1"
echo "0)----magic-number: ${bps_hdr[0]}"
echo "1)-----source-size: ${bps_hdr[1]}"
echo "2)-----target-size: ${bps_hdr[2]}"
echo "3)---manifest-size: ${bps_hdr[3]}"
echo "4)--manifest-index: ${bps_hdr[4]}"
}
# ALIAS for echo_header()
function print_header() {
echo_header "$1"
}
# As String Hex output:
function echo_manifest() {
decoded_message=$(get_header "$1")
declare -a header_array
read -a header_array <<< ${decoded_message}
magic_num="${header_array[0]}"
source_size=${header_array[1]}
target_size=${header_array[2]}
mf_size=${header_array[3]}
mf_index=${header_array[4]}
metadata=$(xxd -p -s ${mf_index} -l ${mf_size} "$1")
printf "${metadata}"
}
# As Binary Output:
function print_manifest() {
echo_manifest "$1" | xxd -r -p
}
function has_manifest() {
[[ -n $(echo_manifest "$1") ]] && true || false
}
function save_manifest() {
local manifest_file="$1"
local header_file=""
if (( $# >= 2 ))
then
header_file="$2"
save_romheader_tofile "$1" "$2"
else
header_file=$(save_romheader_tofile "$1")
fi
echo "${header_file}"
}
function insert_manifest() {
header_file=$(save_manifest "$1")
CLEANUP_FILES+=("${header_file}")
insert_header "${header_file}" "$2"
}
function insert_header() {
cat <(xxd -p "$1") <(xxd -p "$2") | xxd -r -p > "$3"
}
function insert_from() {
hdr=$(save_romheader_tofile "$1")
CLEANUP_FILES+=("${hdr}")
insert_header "${hdr}" "$2"
}
function insert() {
# Check if $2 has a header already
if is_bps "$1" && has_manifest "$1"; then insert_manifest "$1" "$2" "$3"; return; fi
if is_rom_header "$1"; then insert_header "$1" "$2" "$3"; return; fi
if has_rom_header "$1"; then insert_from "$1" "$2" "$3"; return; fi
die "function insert() requires a bps manifest OR nes header file."
}
##########################################
# FLIPS WRAPPER METHODS #
##########################################
# flips -a --manifest=header.nes 'Hack.bps' 'Base.tmp' 'Hack.tmp'
function apply_patch() {
if ! is_bps "$1"; then die "apply_patch() requires a .bps patch file: Not $1"; fi
local patch="$1"
local baserom="$2"
local basetmp=$(get_temp_no_header "$2")
CLEANUP_FILES+=("${basetmp}")
local hack=""
if (( $# >= 3 ))
then
hack="$3"
else
hack=$(getBase "${patch}")
if has_manifest "${patch}"
then
if get_nes_header_binary "${patch}" | is_nes
then
hack="${hack}.nes"
elif get_nes_header_binary "${patch}" | is_fds
then
hack="${hack}.fds"
else
hack="${hack}."$(getExtention "${baserom}")
fi
else
hack="${hack}."$(getExtention "${baserom}")
fi
fi
hacktmp=$(get_temp_no_header "$hack")
CLEANUP_FILES+=("${hacktmp}")
local header="header.tmp"
flips -a --manifest="${header}" "${patch}" "${basetmp}" "${hacktmp}"
[ -f "header.nes" ] && CLEANUP_FILES+=("header.tmp")
if [ ! -f "${hacktmp}" ]; then die "apply_patch() failed on $*"; fi
if [ ! -f "header.tmp" ]; then header=$(save_manifest "${patch}"); fi
if [ ! -f "header.tmp" ]; then die "apply_patch(): insert() failed: header.nes does not exist!"; fi
CLEANUP_FILES+=("${header}")
insert "${header}" "${hacktmp}" "${hack}"
}
# flips -c --manifest=header.nes 'Base.tmp' 'Hack.tmp' 'Hack.bps'
function create_patch() {
local baserom="$1"
local romhack="$2"
local patchname=""
if (( $# >= 3 ))
then
patchname="$3"
else
patchname=$(getBase "${romhack}")".bps"
fi
if ! has_bps_ext "${patchname}"; then die "create_patch() failed: Requires .bps file: Not ${patchname}"; fi
local header=$(save_romheader_tofile "${romhack}")
CLEANUP_FILES+=("${header}")
local basetmp=$(get_temp_no_header "${baserom}")
CLEANUP_FILES+=("${basetmp}")
local hacktmp=$(get_temp_no_header "${romhack}")
CLEANUP_FILES+=("${hacktmp}")
if [ -n "${header}" ] && [ -f "${header}" ]
then
flips -c --manifest="${header}" "${basetmp}" "${hacktmp}" "${patchname}"
else
flips -c "${basetmp}" "${hacktmp}" "${patchname}"
fi
}
##########################################
# 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
declare -a test_values
test_values+=("7-zip archive data, version 0.4")
test_values+=("Zip archive data, at least v2.0 to extract, compression method=deflate")
test_values+=("BPS patch file")
test_values+=("IPS patch file")
test_values+=("IPS patch file")
test_values+=("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)")
test_values+=("data")
test_values+=("UPS patch file")
test_values+=("VCDIFF binary diff")
test_values+=("data")
test_values+=("NES ROM image (iNES): 8x16k PRG, 0x8k CHR [H-mirror] [SRAM]")
test_values+=("Famicom Disk System disk image: FMC-ARM, mfr FFFFFFA4 (Rev.00) (2 sides)")
test_values+=("data")
test_values+=("NES ROM image (UNIF v7 format)")
test_values+=("data")
test_values+=("Sufami Turbo ROM image: \"\276\260\327\2\", ID 010003, series index 1 [FastROM]")
test_values+=("data")
test_values+=("Nintendo 64 ROM image (32-bit byteswapped)")
test_values+=("Nintendo 64 ROM image (V64)")
test_values+=("Nintendo 64 ROM image: \"SUPER MARIO 64 \" (NSME, Rev.00)")
test_values+=("Nintendo GameCube disc image: \"The Legend of Zelda Twilight Princess\" (GZ2E01, Rev.00)")
test_values+=("Nintendo Wii disc image: \"SPORTS PACK for REVOLUTION\" (RSPE01, Rev.01)")
test_values+=("Nintendo Wii disc image (WBFS format): \"SPORTS PACK for REVOLUTION\" (RSPE01, Rev.01)")
test_values+=("Game Boy ROM image: \"ZELDA\" (Rev.02) [MBC1+RAM+BATT], ROM: 4Mbit, RAM: 64Kbit")
test_values+=("Game Boy ROM image: \"PM_CRYSTAL\" (Rev.01) [CGB ONLY] [MBC3+TIMER+RAM+BATT], ROM: 16Mbit, RAM: 256Kbit")
test_values+=("Game Boy Advance ROM image: \"POKEMON RUBY\" (AXVE01, Rev.02)")
test_values+=("Nintendo DS ROM image: \"CASTLEVANIA1\" (ACVPA4, Rev.00) (decrypted)")
test_values+=("Sega Game Gear ROM image: 2408 (Rev.01) (256 KB)")
test_values+=("Sega Master System ROM image: 7076 (Rev.00) (256 KB)")
test_values+=("Sega 32X ROM image: \"DOOM \" (GM MK-84506-00, (C)SEGA 1994.OCT)")
test_values+=("Sega Mega Drive / Genesis ROM image: \"\377\377\377\377\" (\377\377\377\3, \377\377\377\377)")
test_values+=("Sega Mega Drive / Genesis ROM image: \"SONIC THE \" (GM 00001051-02, (C)SEGA 1992.SEP)")
test_values+=("Sega Mega Drive / Genesis ROM image: \"NHL HOCKEY \" (GM T-50236 -00, (C)T-50 1991.MAY)")
test_values+=("Sega Mega Drive / Genesis ROM image: \"EA HOCKEY \" (GM T-50236 -50, (C)T-50 1991.JUN)")
test_values+=("Sega Mega Drive / Genesis ROM image (SMD format): 64x16k blocks, last in series or standalone")
test_values+=("Sega Mega CD disc image: \"SOULSTAR \" (GM T-115035-00, (C)T-1151994.NOV), 2352-byte sectors")
test_values+=("Sega Mega CD disc image: \"SOULSTAR \" (GM T-115035-00, (C)T-1151994.NOV), 2048-byte sectors")
test_values+=("Sega Saturn disc image: \"AREA 51 \" (T-9705H , V1.000) (2352-byte sectors)")
test_values+=("Sega Dreamcast disc image: \"CRAZY TAXI \" (MK-51035 , V1.004) (2352-byte sectors)")
test_values+=("UDF filesystem data (version 1.5) 'FINAL_FANTASY_X'")
##########################################
# 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
#declare -i return_value=$(test_zero)
#echo "return_value=${return_value}"
HACK_DIR="_romhacks"
BASE_DIR="_baseroms"
operation=""
files=()
while [[ $# -gt 0 ]]
do
case $1 in
-a|--apply)
operation="apply"
shift
;;
-c|--create)
operation="create"
shift
;;
-e|--echo-header)
operation="echo-header"
shift
;;
-f|--echo-manifest)
operation="echo-manifest"
shift
;;
-g|--get-filetype)
operation="get-filetype"
shift
;;
-i|--is-filetype)
operation="is-filetype"
shift
;;
-j|--get-alltypes)
showAllFileTypes
pipeout
shift
;;
-n|--insert-manifest)
operation="insert-manifest"
shift
;;
-p|--print-manifest)
operation="print-manifest"
shift
;;
-r|--print-header)
operation="print-header"
shift
;;
-s|--save-manifest)
operation="save-manifest"
shift
;;
-t|--get-temp)
operation="get-temp"
shift
;;
-x|--get-hash)
operation="get-hash"
shift
;;
-z|--get-all-hashes)
operation="get-all-hashes"
shift
;;
-h|--help) displayHelp; close;;
-u|--menu) displayFunctionMenu; close;;
-v|--version) displayVersion; close;;
-*|--*) displayHelp; die "Unknown option $1";;
*) files+=("$1"); shift;;
esac
done
set -- "${files[@]}"
arg_count_nes_cli=$#
##########################################
# TEST & DEBUG #
##########################################
# ... tests here ...
require_args 1
case ${operation} in
echo-header) echo_header "$1"; close;;
echo-manifest) echo_manifest "$1"; close;;
get-filetype) getFileType "$1"; close;;
print-manifest) print_manifest "$1"; close;;
print-header) print_header "$1"; close;;
get-temp) get_temp_no_header "$1"; close;;
get-hash) get_hash "$1"; close;;
get-all-hashes) get_all_hashes; close;;
esac
#--------------------------------------------
require_args 2
if [[ "${operation}" = "save-manifest" ]]
then
save_manifest "$1" "$2"
close
elif [[ "${operation}" = "insert-manifest" ]]
then
insert_manifest "$1" "$2"
close
elif [[ ${operation} = "is-filetype" ]]
then
isFileType "$1" $2
close
fi
#--------------------------------------------
require_args 3
if [[ "${operation}" = "create" ]]
then
create_patch "$1" "$2" "$3"
close
elif [[ "${operation}" = "apply" ]]
then
apply_patch "$1" "$2" "$3"
close
fi
#--------------------------------------------
##########################################
# END #
##########################################
close
@Europia79
Copy link
Author

Automatically detects & saves NES headers on patch creation: This means that patches will work for any NES rom, no matter what header the player has: Whether it's iNes1 (signed/unsigned), iNes2, or other, etc. (UNIF format not currently supported).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment