PSBBN Definitive English Patch 2.0
63
01-Setup.sh
Executable file
@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
echo -e "\e[8;30;100t"
|
||||
clear
|
||||
echo " _____ _ ";
|
||||
echo " / ___| | | ";
|
||||
echo " \ \`--. ___| |_ _ _ _ __ ";
|
||||
echo " \`--. \/ _ \ __| | | | '_ \ ";
|
||||
echo " /\__/ / __/ |_| |_| | |_) |";
|
||||
echo " \____/ \___|\__|\__,_| .__/ ";
|
||||
echo " | | ";
|
||||
echo " |_| ";
|
||||
echo
|
||||
echo "This script installs all dependencies required for the 'PSBBN Installer' and 'Game Installer'."
|
||||
echo "It must be run first."
|
||||
echo
|
||||
echo "Press any key to continue..."
|
||||
read -n 1 -s
|
||||
|
||||
# Update package list and install necessary packages
|
||||
sudo apt update && sudo apt install -y axel imagemagick python3-venv python3-pip nodejs npm
|
||||
if [ $? -ne 0 ]; then
|
||||
echo
|
||||
echo "Error: Package installation failed."
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if mkfs.exfat exists, and install exfat-fuse if not
|
||||
if ! command -v mkfs.exfat &> /dev/null; then
|
||||
echo
|
||||
echo "mkfs.exfat not found. Installing exfat-fuse..."
|
||||
sudo apt install -y exfat-fuse
|
||||
if [ $? -ne 0 ]; then
|
||||
echo
|
||||
echo "Error: Failed to install exfat-fuse."
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Setup Python virtual environment and install Python dependencies
|
||||
python3 -m venv venv
|
||||
if [ $? -ne 0 ]; then
|
||||
echo
|
||||
echo "Error: Failed to create Python virtual environment."
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source venv/bin/activate
|
||||
pip install lz4 natsort
|
||||
if [ $? -ne 0 ]; then
|
||||
echo
|
||||
echo "Error: Failed to install Python dependencies."
|
||||
read -p "Press any key to exit..."
|
||||
deactivate
|
||||
exit 1
|
||||
fi
|
||||
deactivate
|
||||
|
||||
echo
|
||||
echo "Setup completed successfully!"
|
||||
read -p "Press any key to exit..."
|
520
02-PSBBN-Installer.sh
Executable file
@ -0,0 +1,520 @@
|
||||
#!/bin/bash
|
||||
# Set terminal size: 100 columns and 40 rows
|
||||
echo -e "\e[8;40;100t"
|
||||
|
||||
# Set paths
|
||||
TOOLKIT_PATH=$(pwd)
|
||||
ASSETS_DIR="${TOOLKIT_PATH}/assets"
|
||||
INSTALL_LOG="${TOOLKIT_PATH}/PSBBN-installer.log"
|
||||
|
||||
clear
|
||||
|
||||
echo "####################################################################">> ${INSTALL_LOG};
|
||||
date >> ${INSTALL_LOG}
|
||||
|
||||
# Choose the PS2 storage device
|
||||
while true; do
|
||||
clear
|
||||
echo " ______ _________________ _ _ _____ _ _ _ ";
|
||||
echo " | ___ \/ ___| ___ \ ___ \ \ | | |_ _| | | | | | ";
|
||||
echo " | |_/ /\ \`--.| |_/ / |_/ / \| | | | _ __ ___| |_ __ _| | | ___ _ __ ";
|
||||
echo " | __/ \`--. \ ___ \ ___ \ . \` | | || '_ \/ __| __/ _\` | | |/ _ \ '__|";
|
||||
echo " | | /\__/ / |_/ / |_/ / |\ | _| || | | \__ \ || (_| | | | __/ | ";
|
||||
echo " \_| \____/\____/\____/\_| \_/ \___/_| |_|___/\__\__,_|_|_|\___|_| ";
|
||||
echo " ";
|
||||
echo " Written by CosmicScale"
|
||||
echo
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
lsblk -p -o MODEL,NAME,SIZE,LABEL,MOUNTPOINT | tee -a ${INSTALL_LOG}
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
|
||||
read -p "Choose your PS2 HDD from the list above (e.g., /dev/sdx): " DEVICE
|
||||
|
||||
# Validate input
|
||||
if [[ $DEVICE =~ ^/dev/sd[a-z]$ ]]; then
|
||||
echo
|
||||
echo -e "Are you sure you want to write to ${DEVICE}?" | tee -a ${INSTALL_LOG}
|
||||
read -p "This will erase all data on the drive. (yes/no): " CONFIRM
|
||||
if [[ $CONFIRM == "yes" ]]; then
|
||||
break
|
||||
else
|
||||
echo "Aborted." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Find all mounted volumes associated with the device
|
||||
mounted_volumes=$(lsblk -ln -o MOUNTPOINT "$DEVICE" | grep -v "^$")
|
||||
|
||||
# Iterate through each mounted volume and unmount it
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Unmounting volumes associated with $DEVICE..."
|
||||
for mount_point in $mounted_volumes; do
|
||||
echo "Unmounting $mount_point..." | tee -a ${INSTALL_LOG}
|
||||
if sudo umount "$mount_point"; then
|
||||
echo "Successfully unmounted $mount_point." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo "Failed to unmount $mount_point. Please unmount manually." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All volumes unmounted for $DEVICE."
|
||||
|
||||
# URL of the webpage
|
||||
URL="https://archive.org/download/psbbn-definitive-english-patch-v2"
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Checking for latest version of the PSBBN Definitive English patch..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
# Download the HTML of the page
|
||||
HTML_FILE=$(mktemp)
|
||||
wget -O "$HTML_FILE" "$URL" >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
# Extract .gz links and dates into a combined list
|
||||
COMBINED_LIST=$(grep -oP '(?<=<td><a href=")[^"]+\.gz' "$HTML_FILE" | \
|
||||
paste -d' ' <(grep -oP '(?<=<td>)[^<]+(?=</td>)' "$HTML_FILE" | \
|
||||
grep -E '^\d{2}-\w{3}-\d{4}') -)
|
||||
|
||||
# Sort the combined list by date (most recent first), and get the latest file
|
||||
LATEST=$(echo "$COMBINED_LIST" | sort -r | head -n 1 | cut -d' ' -f2)
|
||||
|
||||
if [ -z "$LATEST" ]; then
|
||||
echo "Cound not find latest version."
|
||||
# If $LATEST is empty, check for psbbn-definitive-image*.gz file
|
||||
IMAGE_FILE=$(ls ${ASSETS_DIR}/psbbn-definitive-image*.gz 2>/dev/null)
|
||||
if [ -n "$IMAGE_FILE" ]; then
|
||||
# If image file exists, set LATEST to the image file name
|
||||
LATEST=$(basename "$IMAGE_FILE")
|
||||
echo "Found local file: ${LATEST}" | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
rm "$HTML_FILE"
|
||||
echo "Failed to download PSBBN image file. Aborting." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Latest version of PSBBN Definitive English patch is $LATEST"
|
||||
fi
|
||||
|
||||
# Check for and delete older 'psbbn-definitive-image*.gz' files
|
||||
for file in "${ASSETS_DIR}"/psbbn-definitive-image*.gz; do
|
||||
if [[ -f "$file" && "$(basename "$file")" != "$LATEST" ]]; then
|
||||
echo "Deleting old file: $file" | tee -a ${INSTALL_LOG}
|
||||
rm "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if the file exists in ${ASSETS_DIR}
|
||||
if [[ -f "${ASSETS_DIR}/${LATEST}" && ! -f "${ASSETS_DIR}/${LATEST}.st" ]]; then
|
||||
echo "File ${LATEST} already exists in ${ASSETS_DIR}. Skipping download." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
# Construct the full URL for the .gz file and download it
|
||||
ZIP_URL="$URL/$LATEST"
|
||||
# Proceed with download
|
||||
echo "Downloading ${LATEST}..." | tee -a ${INSTALL_LOG}
|
||||
axel -n 8 -a "$ZIP_URL" -o "${ASSETS_DIR}"
|
||||
|
||||
# Check if the file was downloaded successfully
|
||||
if [[ -f "${ASSETS_DIR}/${LATEST}" && ! -f "${ASSETS_DIR}/${LATEST}.st" ]]; then
|
||||
echo "Download completed: ${LATEST}" | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
rm "$HTML_FILE"
|
||||
echo "Download failed for ${LATEST}. Please check your internet connection and try again." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm "$HTML_FILE"
|
||||
fi
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Checking for POPS binaries..."
|
||||
|
||||
# Check POPS files exist
|
||||
if [[ -f "${ASSETS_DIR}/POPS-binaries-main/POPS.ELF" && -f "${ASSETS_DIR}/POPS-binaries-main/IOPRP252.IMG" ]]; then
|
||||
echo "Both POPS.ELF and IOPRP252.IMG exist in ${ASSETS_DIR}. Skipping download." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo "One or both files are missing in ${ASSETS_DIR}." | tee -a ${INSTALL_LOG}
|
||||
# Check if POPS-binaries-main.zip exists
|
||||
if [[ -f "${ASSETS_DIR}/POPS-binaries-main.zip" && ! -f "${ASSETS_DIR}/POPS-binaries-main.zip.st" ]]; then
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "POPS-binaries-main.zip found in ${ASSETS_DIR}. Extracting..." | tee -a ${INSTALL_LOG}
|
||||
unzip -o ${ASSETS_DIR}/POPS-binaries-main.zip -d ${ASSETS_DIR} >> ${INSTALL_LOG} 2>&1
|
||||
else
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Downloading POPS binaries..." | tee -a ${INSTALL_LOG}
|
||||
axel -a https://archive.org/download/pops-binaries-PS2/POPS-binaries-main.zip -o "${ASSETS_DIR}"
|
||||
unzip -o ${ASSETS_DIR}/POPS-binaries-main.zip -d ${ASSETS_DIR} >> ${INSTALL_LOG} 2>&1
|
||||
fi
|
||||
# Check if both POPS.ELF and IOPRP252.IMG exist after extraction
|
||||
if [[ -f "${ASSETS_DIR}/POPS-binaries-main/POPS.ELF" && -f "${ASSETS_DIR}/POPS-binaries-main/IOPRP252.IMG" ]]; then
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "POPS binaries successfully extracted." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Error: One or both files (POPS.ELF, IOPRP252.IMG) are missing after extraction." | tee -a ${INSTALL_LOG}
|
||||
read -p "You can install POPS manually later. Press any key to continue..." | tee -a ${INSTALL_LOG}
|
||||
fi
|
||||
fi
|
||||
|
||||
PSBBN_IMAGE="${ASSETS_DIR}/${LATEST}"
|
||||
|
||||
# Write the PSBBN image
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Writing the PSBBN image to ${DEVICE}..." | tee -a ${INSTALL_LOG}
|
||||
if gunzip -c ${PSBBN_IMAGE} | sudo dd of=${DEVICE} bs=4M status=progress 2>&1 | tee -a ${INSTALL_LOG} ; then
|
||||
sync
|
||||
echo
|
||||
echo "Verifying installation..."
|
||||
if sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc "${DEVICE}" | grep -q '__common'; then
|
||||
echo "Verification successful. PSBBN image installed successfully." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo "Error: Verification failed on ${DEVICE}." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error: Failed to write the image to ${DEVICE}." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to find available space
|
||||
function function_space() {
|
||||
|
||||
|
||||
output=$(sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc ${DEVICE} 2>&1)
|
||||
|
||||
# Check for the word "aborting" in the output
|
||||
if echo "$output" | grep -q "aborting"; then
|
||||
echo "${DEVICE}: APA partition is broken; aborting." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the "used" value, remove "MB" and any commas
|
||||
used=$(echo "$output" | awk '/used:/ {print $6}' | sed 's/,//; s/MB//')
|
||||
capacity=124416
|
||||
|
||||
# Calculate available space (capacity - used)
|
||||
available=$((capacity - used))
|
||||
}
|
||||
|
||||
# Call the function retreive avaliable space
|
||||
function_space
|
||||
|
||||
# Divide available space by 128 to calculate the maximum number of partitions
|
||||
PP=$(((available - 18560) / 128))
|
||||
|
||||
# Loop until the user enters a valid number of partitions
|
||||
while true; do
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo " #########################################################################################"
|
||||
echo " # 'OPL Launcher' partitions are used to launch games from the 'Game Channel.' #"
|
||||
echo " # Consider how many games you want to install, and plan for future expansion. #"
|
||||
echo " # Additional 'OPL Launcher' partitions cannot be created after setup. #"
|
||||
echo " # #"
|
||||
echo " # Note: The more partitions you create, the longer it will take to load the game list. #"
|
||||
echo " # Fewer 'OPL Launcher' partitions leave more space for the 'Music' partition #"
|
||||
echo " # and the 'POPS' partition for PS1 games. #"
|
||||
echo " # #"
|
||||
echo " # A good starting point is 200 partitions, but feel free to experiment. #"
|
||||
echo " #########################################################################################"
|
||||
echo
|
||||
read -p "Enter the number of \"OPL Launcher\" partitions you would like (1-$PP): " PARTITION_COUNT
|
||||
|
||||
# Check if input is a valid number within the specified range
|
||||
if [[ "$PARTITION_COUNT" =~ ^[0-9]+$ ]] && [ "$PARTITION_COUNT" -ge 1 ] && [ "$PARTITION_COUNT" -le $PP ]; then
|
||||
break # Exit the loop if the input is valid
|
||||
else
|
||||
echo "Invalid input. Please enter a number between 1 and $PP." | tee -a ${INSTALL_LOG}
|
||||
fi
|
||||
done
|
||||
|
||||
GB=$(((available + 2048 - 10368 - (PARTITION_COUNT * 128)) / 1024))
|
||||
|
||||
# Prompt user for partition size for music, validate input, and keep asking until valid input is provided
|
||||
while true; do
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "What size would you like the \"Music\" partition to be?" | tee -a ${INSTALL_LOG}
|
||||
echo "Remaining space will be allocated to the __.POPS partition for PS1 games"
|
||||
echo "Minimum 10 GB, Available space: $GB GB" | tee -a ${INSTALL_LOG}
|
||||
read -p "Enter partition size (in GB): " gb_size
|
||||
|
||||
# Check if the input is a valid number
|
||||
if [[ ! "$gb_size" =~ ^[0-9]+$ ]]; then
|
||||
echo "Invalid input. Please enter a valid number." | tee -a ${INSTALL_LOG}
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if the value is within the valid range
|
||||
if (( gb_size >= 10 && gb_size <= GB )); then
|
||||
echo "Valid partition size: $gb_size GB" | tee -a ${INSTALL_LOG}
|
||||
break # Exit the loop if the input is valid
|
||||
else
|
||||
echo "Invalid size. Please enter a value between 10 and $GB GB." | tee -a ${INSTALL_LOG}
|
||||
fi
|
||||
done
|
||||
|
||||
music_partition=$((gb_size * 1024 - 2048))
|
||||
pops_partition=$((available - (PARTITION_COUNT * 128) - music_partition -128))
|
||||
GB=$((pops_partition / 1024))
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "$GB GB alocated for __.POPS partition." | tee -a ${INSTALL_LOG}
|
||||
|
||||
COMMANDS="device ${DEVICE}\n"
|
||||
COMMANDS+="mkpart __linux.8 ${music_partition}M REISER\n"
|
||||
COMMANDS+="mkpart __.POPS ${pops_partition}M PFS\n"
|
||||
COMMANDS+="mkpart +OPL 128M PFS\nexit"
|
||||
echo -e "$COMMANDS" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
|
||||
# Call the function to retrieve available space
|
||||
function_space
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Creating $PARTITION_COUNT \"OPL Launcher\" partitions..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
# Set starting partition number
|
||||
START_PARTITION_NUMBER=1
|
||||
|
||||
# Initialize a counter for the successfully created partitions
|
||||
successful_count=0
|
||||
|
||||
# Loop to create the specified number of partitions
|
||||
for ((i = 0; i < PARTITION_COUNT; i++)); do
|
||||
# Check if available space is at least 128 MB
|
||||
if [ "$available" -lt 128 ]; then
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Insufficient space for another partition." | tee -a ${INSTALL_LOG}
|
||||
break
|
||||
fi
|
||||
|
||||
# Calculate the current partition number (starting at $START_PARTITION_NUMBER)
|
||||
PARTITION_NUMBER=$((START_PARTITION_NUMBER + i))
|
||||
|
||||
# Generate the partition label dynamically (PP.001, PP.002, etc.)
|
||||
PARTITION_LABEL=$(printf "PP.%03d" "$PARTITION_NUMBER")
|
||||
|
||||
# Build the command to create this partition
|
||||
COMMAND="device ${DEVICE}\nmkpart ${PARTITION_LABEL} 128M PFS\nexit"
|
||||
|
||||
# Run the partition creation command in PFS Shell
|
||||
echo -e "$COMMAND" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
# Increment the count of successfully created partitions
|
||||
((successful_count++))
|
||||
|
||||
# Call function_space after exiting PFS Shell to update the available space
|
||||
function_space
|
||||
done
|
||||
|
||||
# Display the total number of partitions created successfully
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "$successful_count \"OPL Launcher\" partitions created successfully." | tee -a ${INSTALL_LOG}
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Modifying partition headers..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
cd "${TOOLKIT_PATH}/assets/"
|
||||
|
||||
# After partitions are created, modify the header for each partition
|
||||
for ((i = START_PARTITION_NUMBER; i < START_PARTITION_NUMBER + PARTITION_COUNT; i++)); do
|
||||
PARTITION_LABEL=$(printf "PP.%03d" "$i")
|
||||
sudo "${TOOLKIT_PATH}/helper/HDL Dump.elf" modify_header "${DEVICE}" "${PARTITION_LABEL}" >> ${INSTALL_LOG} 2>&1
|
||||
done
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Making \"res\" folders..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
# make 'res' directory on all PP partitions
|
||||
COMMANDS="device ${DEVICE}\n"
|
||||
for ((i = START_PARTITION_NUMBER; i < START_PARTITION_NUMBER + PARTITION_COUNT; i++)); do
|
||||
PARTITION_LABEL=$(printf "PP.%03d" "$i")
|
||||
COMMANDS+="mount ${PARTITION_LABEL}\n"
|
||||
COMMANDS+="mkdir res\n"
|
||||
COMMANDS+="umount\n"
|
||||
done
|
||||
COMMANDS+="exit"
|
||||
|
||||
# Pipe all commands to PFS Shell for mounting, copying, and unmounting
|
||||
echo -e "$COMMANDS" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Installing POPS and OPL..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
# Copy POPS files and OPL to relevent partitions
|
||||
COMMANDS="device ${DEVICE}\n"
|
||||
COMMANDS+="mount +OPL\n"
|
||||
COMMANDS+="put OPNPS2LD.ELF\n"
|
||||
COMMANDS+="umount\n"
|
||||
COMMANDS+="mount __common\n"
|
||||
COMMANDS+="mkdir POPS\n"
|
||||
COMMANDS+="cd POPS\n"
|
||||
COMMANDS+="put IGR_BG.TM2\n"
|
||||
COMMANDS+="put IGR_NO.TM2\n"
|
||||
COMMANDS+="put IGR_YES.TM2\n"
|
||||
COMMANDS+="lcd POPS-binaries-main\n"
|
||||
COMMANDS+="put POPS.ELF\n"
|
||||
COMMANDS+="put IOPRP252.IMG\n"
|
||||
COMMANDS+="cd ..\n"
|
||||
COMMANDS+="umount\n"
|
||||
COMMANDS+="exit"
|
||||
|
||||
# Pipe all commands to PFS Shell for mounting, copying, and unmounting
|
||||
echo -e "$COMMANDS" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
cd "${TOOLKIT_PATH}"
|
||||
|
||||
|
||||
#//////////////////////////////////////////////// APA-Jail code by Berion ////////////////////////////////////////////////
|
||||
|
||||
function function_disk_size_check() {
|
||||
LBA_MAX=$(sudo blockdev --getsize ${DEVICE})
|
||||
if [ ${LBA_MAX} -gt 4294967296 ]; then
|
||||
echo -e "ERROR: Disk size exceeding 2TiB. Formatting aborted." | tee -a ${INSTALL_LOG}
|
||||
function_exit
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function function_apajail_magic_number() {
|
||||
echo ${MAGIC_NUMBER} | xxd -r -p > /tmp/apajail_magic_number.bin
|
||||
sudo dd if=/tmp/apajail_magic_number.bin of=${DEVICE} bs=8 count=1 seek=28 conv=notrunc >> ${INSTALL_LOG} 2>&1
|
||||
}
|
||||
|
||||
function function_make_ps2_dirs() {
|
||||
if [ ! -d "/tmp/ps2_dirs" ]; then
|
||||
mkdir /tmp/ps2_dirs
|
||||
fi
|
||||
if [ "$(echo ${DEVICE} | grep -o /dev/loop)" = "/dev/loop" ]; then
|
||||
sudo mount ${DEVICE}p${PARTITION_NUMBER} /tmp/ps2_dirs
|
||||
else sudo mount ${DEVICE}${PARTITION_NUMBER} /tmp/ps2_dirs
|
||||
fi
|
||||
cd /tmp/ps2_dirs
|
||||
sudo mkdir -p APPS/ # Open PS2 Loader: applications
|
||||
sudo mkdir -p ART/ # Open PS2 Loader: disc covers (<GameID>_COV.png, <GameID>_ICO.png, <GameID>_SCR.png etc.)
|
||||
sudo mkdir -p CFG/ # Open PS2 Loader: per game configs (<GameID>.cfg)
|
||||
sudo mkdir -p CHT/ # Open PS2 Loader: cheats (<GameID>.cht)
|
||||
sudo mkdir -p CD/ # Open PS2 Loader: CD disc images (*.iso, *.zso)
|
||||
sudo mkdir -p DVD/ # Open PS2 Loader: DVD disc images (*.iso, *.zso)
|
||||
sudo mkdir -p LNG # Open PS2 Loader: Language files
|
||||
sudo mkdir -p THM/ # Open PS2 Loader: theme dirs (thm_<ThemeName>/*)
|
||||
sudo mkdir -p VMC/ # Open PS2 Loader: non-ECC PS2 Memory Card images (generic or <GameID>_0.bin, <GameID>_1.bin)
|
||||
sync
|
||||
sudo umount -l /tmp/ps2_dirs
|
||||
}
|
||||
|
||||
function function_apa_checksum_fix() {
|
||||
sudo dd if=${DEVICE} of=/tmp/apa_header_full.bin bs=512 count=2 >> ${INSTALL_LOG} 2>&1
|
||||
"${TOOLKIT_PATH}/helper/PS2 APA Header Checksum Fixer.elf" /tmp/apa_header_full.bin | sed -n 8p | awk '{print $6}' | xxd -r -p > /tmp/apa_header_checksum.bin
|
||||
sudo dd if=/tmp/apa_header_checksum.bin of=${DEVICE} conv=notrunc >> ${INSTALL_LOG} 2>&1
|
||||
}
|
||||
|
||||
function function_clear_temp() {
|
||||
sudo rm /tmp/apa_header_address.bin &> /dev/null
|
||||
sudo rm /tmp/apa_header_boot.bin &> /dev/null
|
||||
sudo rm /tmp/apa_header_checksum.bin &> /dev/null
|
||||
sudo rm /tmp/apa_header_full.bin &> /dev/null
|
||||
sudo rm /tmp/apa_journal.bin &> /dev/null
|
||||
sudo rm /tmp/apa_header_probe.bin &> /dev/null
|
||||
sudo rm /tmp/apa_header_size.bin &> /dev/null
|
||||
sudo rm /tmp/apajail_magic_number.bin &> /dev/null
|
||||
sudo rm /tmp/apa_index.xz &> /dev/null
|
||||
sudo rm /tmp/gpt_2nd.xz &> /dev/null
|
||||
}
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Running APA-Jail by Berion..." | tee -a ${INSTALL_LOG}
|
||||
|
||||
# Hashed out for testing. Larger drive support most likley possible when using a restored disc image from a smaller drive
|
||||
# function_disk_size_check
|
||||
|
||||
# Signature injection (type A2):
|
||||
MAGIC_NUMBER="4150414A2D413200"
|
||||
function_apajail_magic_number
|
||||
|
||||
# Setting up MBR:
|
||||
{
|
||||
echo -e ",128GiB,17\n,32MiB,17\n,,07" | sudo sfdisk ${DEVICE}
|
||||
sudo partprobe ${DEVICE}
|
||||
if [ "$(echo ${DEVICE} | grep -o /dev/loop)" = "/dev/loop" ]; then
|
||||
sudo mkfs.ext2 -L "RECOVERY" ${DEVICE}p2
|
||||
sudo mkfs.exfat -c 32K -L "OPL" ${DEVICE}p3
|
||||
else
|
||||
sleep 4
|
||||
sudo mkfs.ext2 -L "RECOVERY" ${DEVICE}2
|
||||
sudo mkfs.exfat -c 32K -L "OPL" ${DEVICE}3
|
||||
fi
|
||||
} >> ${INSTALL_LOG} 2>&1
|
||||
|
||||
PARTITION_NUMBER=3
|
||||
function_make_ps2_dirs
|
||||
|
||||
# Finalising recovery:
|
||||
if [ ! -d "${TOOLKIT_PATH}/storage/hdd/recovery" ]; then
|
||||
mkdir -p ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
fi
|
||||
if [ "$(echo ${DEVICE} | grep -o /dev/loop)" = "/dev/loop" ]; then
|
||||
sudo mount ${DEVICE}p2 ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
else sudo mount ${DEVICE}2 ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
fi
|
||||
sudo dd if=${DEVICE} bs=128M count=1 status=noxfer 2>> ${INSTALL_LOG} | xz -z > /tmp/apa_index.xz 2>> ${INSTALL_LOG}
|
||||
sudo cp /tmp/apa_index.xz ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
LBA_MAX=$(sudo blockdev --getsize ${DEVICE})
|
||||
LBA_GPT_BUP=$(echo $(($LBA_MAX-33)))
|
||||
sudo dd if=${DEVICE} skip=${LBA_GPT_BUP} bs=512 count=33 status=noxfer 2>> ${INSTALL_LOG} | xz -z > /tmp/gpt_2nd.xz 2>> ${INSTALL_LOG}
|
||||
sudo cp /tmp/gpt_2nd.xz ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
sync
|
||||
sudo umount -l ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
rmdir ${TOOLKIT_PATH}/storage/hdd/recovery
|
||||
|
||||
function_apa_checksum_fix
|
||||
|
||||
function_clear_temp
|
||||
|
||||
unset DEVICE
|
||||
unset LBA_GPT_BUP
|
||||
unset LBA_MAX
|
||||
unset MAGIC_NUMBER
|
||||
unset PARTITION_NUMBER
|
||||
|
||||
|
||||
|
||||
#/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
# Run the command and capture output
|
||||
output=$(sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc ${DEVICE} 2>&1)
|
||||
|
||||
# Check for the word "aborting" in the output
|
||||
if echo "$output" | grep -q "aborting"; then
|
||||
echo "Error: APA partition is broken on ${DEVICE}. Install failed." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc "${DEVICE}" | grep -q '__.POPS' && \
|
||||
sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc "${DEVICE}" | grep -q '__linux.8' && \
|
||||
sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc "${DEVICE}" | grep -q "PP.${PARTITION_COUNT}" && \
|
||||
sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc "${DEVICE}" | grep -q '+OPL'; then
|
||||
echo "All partitions were created successfully." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo "Error: Some partitions are missing on ${DEVICE}." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if 'OPL' is found in the 'lsblk' output and if it matches the device
|
||||
if ! lsblk -p -o NAME,LABEL | grep "OPL" | grep -q "${DEVICE}"; then
|
||||
echo "Error: APA-Jail failed on ${DEVICE}." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
read -p "PSBBN successfully installed. Press any key to exit. " | tee -a ${INSTALL_LOG}
|
463
03-Game-Installer.sh
Executable file
@ -0,0 +1,463 @@
|
||||
#!/bin/bash
|
||||
# Set terminal size: 100 columns and 40 rows
|
||||
echo -e "\e[8;40;100t"
|
||||
|
||||
# Set paths
|
||||
TOOLKIT_PATH=$(pwd)
|
||||
ICONS_DIR="${TOOLKIT_PATH}/icons"
|
||||
ARTWORK_DIR="${ICONS_DIR}/art"
|
||||
POPSTARTER_DIR="${TOOLKIT_PATH}/assets/POPSTARTER.ELF"
|
||||
LOG_FILE="${TOOLKIT_PATH}/game-installer.log"
|
||||
MISSING_ART=${TOOLKIT_PATH}/missing-art.log
|
||||
|
||||
|
||||
# Modify this path if your games are stored in a different location:
|
||||
GAMES_PATH="${TOOLKIT_PATH}/games"
|
||||
|
||||
POPS_FOLDER="${GAMES_PATH}/POPS"
|
||||
ALL_GAMES="${GAMES_PATH}/master.list"
|
||||
|
||||
cd ${TOOLKIT_PATH}
|
||||
|
||||
clear
|
||||
|
||||
date >> ${LOG_FILE}
|
||||
|
||||
# Choose the PS2 storage device
|
||||
while true; do
|
||||
clear
|
||||
echo "####################################################################">> ${LOG_FILE};
|
||||
echo " _____ _____ _ _ _ ";
|
||||
echo " | __ \ |_ _| | | | | | ";
|
||||
echo " | | \/ __ _ _ __ ___ ___ | | _ __ ___| |_ __ _| | | ___ ___ ";
|
||||
echo " | | __ / _\` | '_ \` _ \ / _ \ | || '_ \/ __| __/ _\` | | |/ _ \ __|";
|
||||
echo " | |_\ \ (_| | | | | | | __/ _| || | | \__ \ || (_| | | | __/ | ";
|
||||
echo " \____/\__,_|_| |_| |_|\___| \___/_| |_|___/\__\__,_|_|_|\___|_| ";
|
||||
echo " ";
|
||||
echo " Written by CosmicScale"
|
||||
echo | tee -a ${LOG_FILE}
|
||||
lsblk -p -o MODEL,NAME,SIZE,LABEL,MOUNTPOINT | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
read -p "Choose your PS2 HDD from list above e.g. /dev/sdx): " DEVICE
|
||||
|
||||
# Validate input
|
||||
if [[ $DEVICE =~ ^/dev/sd[a-z]$ ]]; then
|
||||
echo
|
||||
echo -e "Selected drive: \"${DEVICE}\"" | tee -a ${LOG_FILE}
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Find all mounted volumes associated with the device
|
||||
mounted_volumes=$(lsblk -ln -o MOUNTPOINT "$DEVICE" | grep -v "^$")
|
||||
|
||||
# Iterate through each mounted volume and unmount it
|
||||
echo | tee -a ${INSTALL_LOG}
|
||||
echo "Unmounting volumes associated with $DEVICE..."
|
||||
for mount_point in $mounted_volumes; do
|
||||
echo "Unmounting $mount_point..." | tee -a ${INSTALL_LOG}
|
||||
if sudo umount "$mount_point"; then
|
||||
echo "Successfully unmounted $mount_point." | tee -a ${INSTALL_LOG}
|
||||
else
|
||||
echo "Failed to unmount $mount_point. Please unmount manually." | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All volumes unmounted for $DEVICE."| tee -a ${INSTALL_LOG}
|
||||
|
||||
# Validate the GAMES_PATH
|
||||
if [[ ! -d "$GAMES_PATH" ]]; then
|
||||
echo
|
||||
echo "Error: GAMES_PATH is not a valid directory: $GAMES_PATH" | tee -a ${LOG_FILE}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "GAMES_PATH is valid: $GAMES_PATH" | tee -a ${LOG_FILE}
|
||||
|
||||
# Check if the file exists
|
||||
if [ -f "./venv/bin/activate" ]; then
|
||||
echo "The Python virtual environment exists."
|
||||
else
|
||||
echo "Error: The Python virtual environment does not exist."
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Activate the virtual environment
|
||||
source "./venv/bin/activate"
|
||||
|
||||
# Check if activation was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to activate the virtual environment" | tee -a ${INSTALL_LOG}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create games list of PS1 and PS2 games to be installed
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Creating PS1 games list..."| tee -a ${LOG_FILE}
|
||||
python3 ./helper/list-builder-ps1.py ${GAMES_PATH} | tee -a ${LOG_FILE}
|
||||
echo "Creating PS2 games list..."| tee -a ${LOG_FILE}
|
||||
python3 ./helper/list-builder-ps2.py ${GAMES_PATH} | tee -a ${LOG_FILE}
|
||||
|
||||
# Deactivate the virtual environment
|
||||
deactivate
|
||||
|
||||
# Create master list combining PS1 and PS2 games to a single list
|
||||
if [[ ! -f "${GAMES_PATH}/ps1.list" && ! -f "${GAMES_PATH}/ps2.list" ]]; then
|
||||
echo "No games found to install."| tee -a "${LOG_FILE}"
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
elif [[ ! -f "${GAMES_PATH}/ps1.list" ]]; then
|
||||
cat "${GAMES_PATH}/ps2.list" > "${GAMES_PATH}/master.list" 2>> "${LOG_FILE}"
|
||||
else
|
||||
cat "${GAMES_PATH}/ps1.list" > "${GAMES_PATH}/master.list" 2>> "${LOG_FILE}"
|
||||
cat "${GAMES_PATH}/ps2.list" >> "${GAMES_PATH}/master.list" 2>> "${LOG_FILE}"
|
||||
fi
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Games list successfully created"| tee -a ${LOG_FILE}
|
||||
|
||||
# Count the number of games to be installed
|
||||
count=$(grep -c '^[^[:space:]]' ${ALL_GAMES})
|
||||
echo "Number of games to install: $count" | tee -a ${LOG_FILE}
|
||||
|
||||
# Count the number of 'OPL Launcher' partitions available
|
||||
partition_count=$(sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc $DEVICE | grep -o 'PP\.[0-9]\+' | grep -v '^$' | wc -l)
|
||||
|
||||
echo "Number of PP partitions: $partition_count" | tee -a ${LOG_FILE}
|
||||
|
||||
# Check if the count exceeds the partition count
|
||||
if [ "$count" -gt "$partition_count" ]; then
|
||||
echo
|
||||
echo "Error: Number of games ($count) exceeds the available partitions ($partition_count)." | tee -a "${LOG_FILE}"
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the list of partition names
|
||||
partitions=$(sudo ${TOOLKIT_PATH}/helper/HDL\ Dump.elf toc $DEVICE | grep -o 'PP\.[0-9]*')
|
||||
|
||||
missing_partitions=()
|
||||
|
||||
# Check for each partition from PP.001 to PP.<partition_count> and identify any missing partitions
|
||||
for i in $(seq -f "%03g" 1 "$partition_count"); do
|
||||
partition_name="PP.$i"
|
||||
if ! echo "$partitions" | grep -q "$partition_name"; then
|
||||
missing_partitions+=("$partition_name")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#missing_partitions[@]} -eq 0 ]; then
|
||||
echo "All partitions (PP.001 to PP.${partition_count}) are present." | tee -a ${LOG_FILE}
|
||||
else
|
||||
echo "Missing partitions:" | tee -a ${LOG_FILE}
|
||||
for partition in "${missing_partitions[@]}"; do
|
||||
echo "$partition" | tee -a ${LOG_FILE}
|
||||
read -p "Press any key to exit..."
|
||||
exit 1
|
||||
done
|
||||
fi
|
||||
|
||||
# Set the highest starting partition based on available PP partitions
|
||||
START_PARTITION_NUMBER=$(echo "$partitions" | grep -o 'PP\.[0-9]*' | sort -V | tail -n 1 | sed 's/PP\.//')
|
||||
START_PARTITION_NUMBER=$((START_PARTITION_NUMBER))
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Ready to install games. Press any key to continue..."
|
||||
read -n 1 -s
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Preparing to sync PS1 games..." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
# Step 1: Create matching .ELF files for .VCD files
|
||||
echo "Creating matching .ELF files for .VCDs..." | tee -a ${LOG_FILE}
|
||||
for vcd_file in "$POPS_FOLDER"/*.VCD; do
|
||||
if [ -f "$vcd_file" ]; then
|
||||
# Extract the base name (without extension) from the .VCD file
|
||||
base_name=$(basename "$vcd_file" .VCD)
|
||||
# Define the corresponding .ELF file name
|
||||
elf_file="$POPS_FOLDER/$base_name.ELF"
|
||||
# Copy and rename POPSTARTER.ELF to match the .VCD file
|
||||
if [ ! -f "$elf_file" ]; then
|
||||
echo "Creating $elf_file..." | tee -a ${LOG_FILE}
|
||||
cp "$POPSTARTER_DIR" "$elf_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Matching .ELF files created successfully." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
# Step 2: Delete .ELF files without matching .VCD files
|
||||
echo "Removing orphan .ELF files..." | tee -a ${LOG_FILE}
|
||||
for elf_file in "$POPS_FOLDER"/*.ELF; do
|
||||
if [ -f "$elf_file" ]; then
|
||||
# Extract the base name (without extension) from the .ELF file
|
||||
base_name=$(basename "$elf_file" .ELF)
|
||||
# Check if a corresponding .VCD file exists
|
||||
vcd_file="$POPS_FOLDER/$base_name.VCD"
|
||||
if [ ! -f "$vcd_file" ]; then
|
||||
echo "Deleting orphan $elf_file..." | tee -a ${LOG_FILE}
|
||||
rm "$elf_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "Orphan .ELF files removed successfully." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
# Generate the local file list directly in a variable
|
||||
local_files=$(ls -1 $POPS_FOLDER | sort)
|
||||
|
||||
# Build the commands for PFS Shell
|
||||
COMMANDS="device ${DEVICE}\n"
|
||||
COMMANDS+="mount __.POPS\n"
|
||||
COMMANDS+="ls\n"
|
||||
COMMANDS+="umount\n"
|
||||
COMMANDS+="exit"
|
||||
|
||||
# Get the PS1 file list directly from PFS Shell output, filtered and sorted
|
||||
ps2_files=$(echo -e "$COMMANDS" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" 2>/dev/null | grep -iE "\.vcd$|\.elf$" | sort)
|
||||
|
||||
# Compute differences and store them in variables
|
||||
files_only_in_local=$(comm -23 <(echo "$local_files") <(echo "$ps2_files"))
|
||||
files_only_in_ps2=$(comm -13 <(echo "$local_files") <(echo "$ps2_files"))
|
||||
|
||||
echo "Files to delete:" | tee -a ${LOG_FILE}
|
||||
echo "$files_only_in_ps2" | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Files to copy:" | tee -a ${LOG_FILE}
|
||||
echo "$files_only_in_local" | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
# Syncing PS1 games
|
||||
cd $POPS_FOLDER
|
||||
combined_commands="device ${DEVICE}\n"
|
||||
combined_commands+="mount __.POPS\n"
|
||||
|
||||
# Add delete commands for files_only_in_ps2
|
||||
if [ -n "$files_only_in_ps2" ]; then
|
||||
while IFS= read -r file; do
|
||||
combined_commands+="rm \"$file\"\n"
|
||||
done <<< "$files_only_in_ps2"
|
||||
else
|
||||
echo "No files to delete." | tee -a ${LOG_FILE}
|
||||
fi
|
||||
echo | tee -a ${LOG_FILE}
|
||||
# Add put commands for files_only_in_local
|
||||
if [ -n "$files_only_in_local" ]; then
|
||||
while IFS= read -r file; do
|
||||
combined_commands+="put \"$file\"\n"
|
||||
done <<< "$files_only_in_local"
|
||||
else
|
||||
echo "No files to copy." | tee -a ${LOG_FILE}
|
||||
fi
|
||||
|
||||
combined_commands+="umount\n"
|
||||
combined_commands+="exit"
|
||||
|
||||
# Execute the combined commands with PFS Shell
|
||||
echo "Syncing PS1 games to HDD..." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo -e "$combined_commands" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${LOG_FILE} 2>&1
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "PS1 games synced sucessfully." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
cd ${TOOLKIT_PATH}
|
||||
|
||||
# Syncing PS2 games
|
||||
echo "Mounting OPL partition" | tee -a ${LOG_FILE}
|
||||
mkdir ${TOOLKIT_PATH}/OPL 2>> "${LOG_FILE}"
|
||||
sudo mount ${DEVICE}3 ${TOOLKIT_PATH}/OPL
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Syncing PS2 games..." | tee -a ${LOG_FILE}
|
||||
sudo rsync -r --progress --ignore-existing --delete ${GAMES_PATH}/CD/ ${TOOLKIT_PATH}/OPL/CD/ | tee -a ${LOG_FILE}
|
||||
sudo rsync -r --progress --ignore-existing --delete ${GAMES_PATH}/DVD/ ${TOOLKIT_PATH}/OPL/DVD/ | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "PS2 games sucessfully synced" | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Unmounting OPL partition..." | tee -a ${LOG_FILE}
|
||||
sudo umount ${TOOLKIT_PATH}/OPL
|
||||
echo | tee -a ${LOG_FILE}
|
||||
|
||||
mkdir -p "${ARTWORK_DIR}/tmp" 2>> "${LOG_FILE}"
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Downloading artwork..." | tee -a ${LOG_FILE}
|
||||
|
||||
# First loop: Run the art downloader script for each game_id if artwork doesn't already exist
|
||||
while IFS='|' read -r game_title game_id publisher disc_type file_name; do
|
||||
# Check if the artwork file already exists
|
||||
png_file="${ARTWORK_DIR}/${game_id}.png"
|
||||
if [[ -f "$png_file" ]]; then
|
||||
echo "Artwork for game ID $game_id already exists. Skipping download." | tee -a ${LOG_FILE}
|
||||
else
|
||||
# If the file doesn't exist, run the art downloader
|
||||
echo "Running art downloader for game ID: $game_id" | tee -a ${LOG_FILE}
|
||||
node ${TOOLKIT_PATH}/helper/art_downloader.js "$game_id" | tee -a ${LOG_FILE}
|
||||
fi
|
||||
done < "$ALL_GAMES"
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Converting artwork..." | tee -a ${LOG_FILE}
|
||||
|
||||
# Define input directory
|
||||
input_dir="${ARTWORK_DIR}/tmp"
|
||||
|
||||
# Check if the directory contains any files
|
||||
if compgen -G "${input_dir}/*" > /dev/null; then
|
||||
for file in "${input_dir}"/*; do
|
||||
# Extract the base filename without the path or extension
|
||||
base_name=$(basename "${file%.*}")
|
||||
|
||||
# Define output filename with .png extension
|
||||
output="${ARTWORK_DIR}/${base_name}.png"
|
||||
|
||||
# Convert each file to .png with resizing and 8-bit depth
|
||||
convert "$file" -resize 256x256 -depth 8 -dither FloydSteinberg -colors 256 "$output" | tee -a ${LOG_FILE}
|
||||
done
|
||||
rm "${input_dir}"/*
|
||||
else
|
||||
echo "No files to process in ${input_dir}" | tee -a ${LOG_FILE}
|
||||
fi
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Creating game assets..." | tee -a ${LOG_FILE}
|
||||
|
||||
# Read the file line by line
|
||||
while IFS='|' read -r game_title game_id publisher disc_type file_name; do
|
||||
# Create a sub-folder named after the game_id
|
||||
game_dir="$ICONS_DIR/$game_id"
|
||||
mkdir -p "$game_dir" | tee -a ${LOG_FILE}
|
||||
|
||||
# Generate the launcher.cfg file
|
||||
launcher_cfg_filename="$game_dir/launcher.cfg"
|
||||
cat > "$launcher_cfg_filename" <<EOL
|
||||
file_name=$file_name
|
||||
title_id=$game_id
|
||||
disc_type=$disc_type
|
||||
EOL
|
||||
echo "Created info.sys: $launcher_cfg_filename" | tee -a ${LOG_FILE}
|
||||
|
||||
# Generate the info.sys file
|
||||
info_sys_filename="$game_dir/info.sys"
|
||||
cat > "$info_sys_filename" <<EOL
|
||||
title = $game_title
|
||||
title_id = $game_id
|
||||
title_sub_id = 0
|
||||
release_date =
|
||||
developer_id =
|
||||
publisher_id = $publisher
|
||||
note =
|
||||
content_web =
|
||||
image_topviewflag = 0
|
||||
image_type = 0
|
||||
image_count = 1
|
||||
image_viewsec = 600
|
||||
copyright_viewflag = 1
|
||||
copyright_imgcount = 1
|
||||
genre =
|
||||
parental_lock = 1
|
||||
effective_date = 0
|
||||
expire_date = 0
|
||||
violence_flag = 0
|
||||
content_type = 255
|
||||
content_subtype = 0
|
||||
EOL
|
||||
echo "Created info.sys: $info_sys_filename" | tee -a ${LOG_FILE}
|
||||
|
||||
# Copy the matching .png file and rename it to jkt_001.png
|
||||
png_file="${TOOLKIT_PATH}/icons/art/${game_id}.png"
|
||||
if [[ -f "$png_file" ]]; then
|
||||
cp "$png_file" "$game_dir/jkt_001.png"
|
||||
echo "Artwork found for $game_title" | tee -a ${LOG_FILE}
|
||||
else
|
||||
if [[ "$disc_type" == "POPS" ]]; then
|
||||
cp "${TOOLKIT_PATH}/icons/art/ps1.png" "$game_dir/jkt_001.png"
|
||||
echo "Artwork not found for $game_title. Using default PS1 image" | tee -a ${LOG_FILE}
|
||||
echo "$game_id $game_title" >> ${MISSING_ART}
|
||||
else
|
||||
cp "${TOOLKIT_PATH}/icons/art/ps2.png" "$game_dir/jkt_001.png"
|
||||
echo "Artwork not found for $game_title. Using default PS2 image" | tee -a ${LOG_FILE}
|
||||
echo "$game_id $game_title" >> ${MISSING_ART}
|
||||
fi
|
||||
fi
|
||||
|
||||
done < "$ALL_GAMES"
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "All .cfg, info.sys, and .png files have been created in their respective sub-folders." | tee -a ${LOG_FILE}
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Installing game assets..." | tee -a ${LOG_FILE}
|
||||
|
||||
cd ${ICONS_DIR}
|
||||
|
||||
# Build the mount/copy/unmount commands for all partitions
|
||||
COMMANDS="device ${DEVICE}\n"
|
||||
i=0
|
||||
while IFS='|' read -r game_title game_id publisher disc_type file_name; do
|
||||
# Calculate the partition label for the current iteration, starting from the highest partition and counting down
|
||||
PARTITION_LABEL=$(printf "PP.%03d" "$((START_PARTITION_NUMBER - i))")
|
||||
COMMANDS+="mount ${PARTITION_LABEL}\n"
|
||||
COMMANDS+="cd ..\n"
|
||||
COMMANDS+="rm OPL-Launcher-BDM.KELF\n"
|
||||
COMMANDS+="put OPL-Launcher-BDM.KELF\n"
|
||||
|
||||
# Navigate into the sub-directory named after the gameid
|
||||
COMMANDS+="lcd ./${game_id}\n"
|
||||
COMMANDS+="rm 'launcher.cfg'\n"
|
||||
COMMANDS+="put 'launcher.cfg'\n"
|
||||
COMMANDS+="cd res\n"
|
||||
COMMANDS+="rm info.sys\n"
|
||||
COMMANDS+="put info.sys\n"
|
||||
COMMANDS+="rm jkt_001.png\n"
|
||||
COMMANDS+="put jkt_001.png\n"
|
||||
COMMANDS+="umount\n"
|
||||
COMMANDS+="lcd ..\n"
|
||||
|
||||
# Increment the loop counter
|
||||
((i++))
|
||||
done < "$ALL_GAMES"
|
||||
|
||||
# Process remaining partitions after the games
|
||||
for ((j = START_PARTITION_NUMBER - i; j >= 1; j--)); do
|
||||
PARTITION_LABEL=$(printf "PP.%03d" "$j")
|
||||
COMMANDS+="mount ${PARTITION_LABEL}\n"
|
||||
COMMANDS+="cd ..\n"
|
||||
COMMANDS+="rm OPL-Launcher-BDM.KELF\n"
|
||||
COMMANDS+="put OPL-Launcher-BDM.KELF\n"
|
||||
COMMANDS+="rm 'launcher.cfg'\n"
|
||||
COMMANDS+="cd res\n"
|
||||
COMMANDS+="rm info.sys\n"
|
||||
COMMANDS+="put info.sys\n"
|
||||
COMMANDS+="rm jkt_001.png\n"
|
||||
COMMANDS+="umount\n"
|
||||
done
|
||||
|
||||
COMMANDS+="exit"
|
||||
|
||||
# Pipe all commands to PFS Shell for mounting, copying, and unmounting
|
||||
echo -e "$COMMANDS" | sudo "${TOOLKIT_PATH}/helper/PFS Shell.elf" >> ${LOG_FILE} 2>&1
|
||||
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Cleaning up..." | tee -a ${LOG_FILE}
|
||||
|
||||
# Loop through all items in the target directory
|
||||
for item in "$ICONS_DIR"/*; do
|
||||
# Check if the item is a directory and not the 'art' folder
|
||||
if [ -d "$item" ] && [ "$(basename "$item")" != "art" ]; then
|
||||
rm -rf "$item" >> ${LOG_FILE} 2>&1
|
||||
fi
|
||||
done
|
||||
|
||||
echo | tee -a ${LOG_FILE}
|
||||
echo "Game installer script complete" | tee -a ${LOG_FILE}
|
||||
echo
|
||||
read -p "Press any key to continue..."
|
104
README.md
@ -1,29 +1,110 @@
|
||||
# PlayStation Broadband Navigator (PSBBN) Definitive English Patch
|
||||
# PSBBN Definitive English Patch
|
||||
|
||||
This is the definitive English patch for Sony's PlayStation Broadband Navigator (PSBBN) software for the PlayStation 2 (PS2) video game console.
|
||||
This is the definitive English patch of Sony's "PlayStation Broadband Navigator" (PSBBN) software for the "PlayStation 2" (PS2) video game console.
|
||||
|
||||
You can find out more about the PSBBN software on Wikipedia [here](https://en.wikipedia.org/wiki/PlayStation_Broadband_Navigator).
|
||||
You can find out more about the PSBBN software on [Wikipedia](https://en.wikipedia.org/wiki/PlayStation_Broadband_Navigator).
|
||||
|
||||
## Patch Features
|
||||
- A full English translation of the stock Japanese BB Navigator version 0.32
|
||||
- All binaries, XML files, textures, and pictures have been translated*
|
||||
- Compatible with any fat model PS2 console regardless of region**
|
||||
- DNAS authorization checks bypassed to enable online connectivity
|
||||
- Access working mirrors for the online game channels from Sony, Hudson, EA, Konami, Capcom, Namco, and KOEI. Hosted courtesy of vitas155 at psbbn.ru
|
||||
- English translations of the online game channels from Sony, Hudson, EA, Konami, Capcom, Namco, and KOEI. Hosted courtesy of vitas155 at [psbbn.ru](https://psbbn.ru/)
|
||||
- "Audio Player" feature re-added to the Music Channel from an earlier release of PSBBN, allowing compatibility with NetMD MiniDisc Recorders
|
||||
- Associated manual pages and troubleshooting regarding the "Audio Player" feature translated and re-added to the user guide
|
||||
- Japanese qwerty on-screen keyboard replaced with US English on-screen keyboard
|
||||
|
||||
Video demonstrating how PSBBN can be used in 2024. **Note**: Additional software and setup is required to achieve everything shown in this video. Click to watch:
|
||||
## New to version 2.0
|
||||
- Large HDD support. No longer limited to 128 GB, now with support for drives up to 128 PB
|
||||
- Supports 700 games, all launchable from the Game Channel
|
||||
- Set a custom size for your music partition. Original limit of 5 GB allowed the storage of around 7 albums. Now the partition can be up to 97 GB for around 150 albums
|
||||
- exFAT partition for easy install of PS2 games
|
||||
- wLaunchELF is pre-installed
|
||||
- PS2 Linux is pre-installed. Just hold any button on the controller at startup to boot into Linux
|
||||
- Bandai and SCEI online channels have been added to the Game Channel
|
||||
- Some minor fixes to the English translation
|
||||
|
||||
[![IMAGE ALT TEXT HERE](https://github.com/user-attachments/assets/298c8c0b-5726-4485-840d-9d567498fd95)](https://www.youtube.com/watch?v=kR1MVcAkW5M)
|
||||
Video demonstrating how PSBBN can be used in 2024:
|
||||
|
||||
[![PSBBN in 2024](https://github.com/user-attachments/assets/298c8c0b-5726-4485-840d-9d567498fd95)](https://www.youtube.com/watch?v=kR1MVcAkW5M)
|
||||
|
||||
## New installation scripts
|
||||
|
||||
These scripts are essential for unlocking all the new features exclusive to version 2.0. They require a Linux environment to run. If Linux is not installed on your PC, you can use a bootable USB drive or a virtual machine. Only Debian-based distributions are supported, with Linux Mint being the recommended choice. You will require a HDD/SSD for your PS2 that is larger than 128 GB, ideally 500 GB or larger. I highly recommend a SSD for better performance. The HDD/SSD can be connected to your PC internally or via USB.
|
||||
|
||||
### Setup script:
|
||||
`01-Setup.sh` installs all the necessary dependencies for the other scripts and must be run first.
|
||||
|
||||
### PSBBN installer script:
|
||||
`02-PSBBN-Installer.sh` fully automates the installation of PSBBN
|
||||
|
||||
- Downloads the latest version of the `PSBBN Definitive English patch` from archive.org and installs it
|
||||
- Asks how many 'OPL Launcher' partitions you'd like to create (for up to 700 games!)
|
||||
- Asks how large you’d like the music partition
|
||||
- Creates a __.POPS partition for PS1 games with the remaining space up to 128 GB
|
||||
- Installs a custom build of OPL with exFAT and Auto Launch support for BDM devices
|
||||
- Installs POPStarter
|
||||
- Runs APA-Jail and creates an exFAT partition with all remaining disk space
|
||||
|
||||
### Game installer script:
|
||||
`03-Game-Installer.sh` fully automates the installation of PS1 and PS2 games. In the `games` folder on your computer, simply put your PS2 ISO or ZSO files in the `CD`/`DVD` folders, and your PS1 VCD files in the `POPS` folder.
|
||||
|
||||
The script will:
|
||||
- Syncs all games in those folders with your PS2's drive
|
||||
- Create all game assets
|
||||
- Download artwork
|
||||
- Install [OPL Launcher BDM](https://github.com/CosmicScale/OPL-Launcher-BDM) into every game partition, making games bootable from the Game Channel
|
||||
|
||||
To add or delete games, simply add or remove them from the `games` folder on your computer, then run the script again to sync. All games are kept in alphabetical order and grouped by series in the Game Channel on PSBBN.
|
||||
|
||||
By default the `games` folder is located in the same directory you installed the scrips to. If you need to change the location of the `games` folder, edit `03-Game-Installer.sh` and modify the `GAMES_PATH` variable.
|
||||
|
||||
|
||||
### Notes:
|
||||
- PSBBN requires a Fat PS2 console with expansion bay and an official Sony Network Adapter
|
||||
- I would highly recommend using a **Kaico IDE to SATA Upgrade Kit** and a SATA SSD such as the **Kingston A400 960G**. The improved random access speed over a HDD really makes a big difference to the responsiveness of the PSBBN interface.
|
||||
- Delete any existing OPL config files you may have from your memory cards
|
||||
- Remove any attached BDM (USB, iLink, MX4ISO) devices from your PS2 console before launching games from the Game Channel
|
||||
- Games in the Game Channel listed as "Coming soon..." will launch OPL if selected
|
||||
- The `root` password for Linux is `password`. There is also a `ps2` user account with the password set as `password`
|
||||
|
||||
### Notes on APA-Jail:
|
||||
APA-Jail, created and developed by [Berion](https://www.psx-place.com/resources/authors/berion.1431/), enables the PS2's APA partitions to coexist with an exFAT partition. This setup allows PSBBN to access the first 128 GB of the HDD/SSD directly. The remaining space on the drive is formatted as an exFAT partition, which can be accessed directly on a PC and by a custom version of [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) on PS2. PS2 games in the ISO and ZSO format are stored on the exFAT partition.
|
||||
|
||||
![APA-Jail](https://user-images.githubusercontent.com/33134408/131233166-9527f892-40bb-4a83-a528-6b733087cf33.png)
|
||||
|
||||
An application called [OPL Launcher BDM](https://github.com/CosmicScale/OPL-Launcher-BDM) resides on the APA partitions, along with a custom build of [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader).
|
||||
|
||||
[OPL Launcher BDM](https://github.com/CosmicScale/OPL-Launcher-BDM) directs [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) to launch specific PS2 games.
|
||||
|
||||
### Warning: Deleting or creating partitions on your PS2 drive will cause drive corruption.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
- PSBBN Definitive English Patch by [CosmicScale](https://github.com/CosmicScale)
|
||||
- Online channels re-translated by [CosmicScale](https://github.com/CosmicScale)
|
||||
- Online channels resurrected, maintained and hosted by vitas155 at [psbbn.ru](https://psbbn.ru/)
|
||||
- `01-Setup.sh`, `02-PSBBN-Installer.sh`, `03-Game-Installer.sh`, `art_downloader.js` written by [CosmicScale](https://github.com/CosmicScale)
|
||||
- Contains code from `list_builder.py` from [XEB+ neutrino Launcher Plugin](https://github.com/MegaBitmap/UDPBD-for-XEBP) by [MegaBitmap](https://github.com/MegaBitmap), modified by [CosmicScale](https://github.com/CosmicScale)
|
||||
- Contains data from `TitlesDB_PS1_English.txt` and `TitlesDB_PS2_English.txt` from the [Title Database Scrapper](https://github.com/GDX-X/Title-Database-Scrapper), modified by [CosmicScale](https://github.com/CosmicScale)
|
||||
- [OPL Launcher BDM](https://github.com/CosmicScale/OPL-Launcher-BDM) written by [CosmicScale](https://github.com/CosmicScale)
|
||||
- Custom build of [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) with BDM contributions from [KrahJohlito](https://github.com/KrahJohlito)
|
||||
- Uses APA-Jail code from the [PS2 HDD Decryption Helper](https://www.psx-place.com/resources/ps2-hdd-decryption-helper.1507/) by [Berion](https://www.psx-place.com/resources/authors/berion.1431/)
|
||||
- `APA Partition Header Checksumer` by Pinky from the [PS2 HDD Decryption Helper](https://www.psx-place.com/resources/ps2-hdd-decryption-helper.1507/) project. Linux port by Bucanero
|
||||
- `ziso.py` from [Open PS2 Loader](https://github.com/ps2homebrew/Open-PS2-Loader) written by Virtuous Flame
|
||||
- [PFS Shell](https://github.com/ps2homebrew/pfsshell)
|
||||
- [HDL Dump](https://github.com/ps2homebrew/hdl-dump)
|
||||
|
||||
---
|
||||
---
|
||||
<details>
|
||||
<summary><font size="4"><b>Legacy versions of the PSBBN Definitive English Patch</b></font></summary>
|
||||
|
||||
## Version History
|
||||
|
||||
### v1.2 - 4th September 2024
|
||||
- Fixed a bug on the Photo Channel that could potentially prevented the Digital Camera feature from being launched.
|
||||
- Fixed a bug on the Photo Channel that could potentially prevent the Digital Camera feature from being launched.
|
||||
- Fixed formatting issues with a number of error messages where text was too long to fit on the screen.
|
||||
- Various small adjustments and corrections to the translation throughout.
|
||||
|
||||
@ -106,7 +187,7 @@ Remove your Free McBoot Memory Card. Power the console on and enjoy PSBBN in ful
|
||||
|
||||
There are a number of ways this can be achieved. On a Japanese PlayStation 2 console with an **official PSBBN installation disc**, or with **Sony Utility Discs Compilation 3**.
|
||||
|
||||
To install via **Sony Utility Discs Compilation 3** you will need a way to boot backup discs on your console, be that a mod chip or a swap disc. If your are lucky enough to have a **SCPH-500xx** series console you can use the **MechaPwn** softmod.
|
||||
To install via **Sony Utility Discs Compilation 3** you will need a way to boot backup discs on your console, be that a mod chip or a swap disc. If you are lucky enough to have a **SCPH-500xx** series console you can use the **MechaPwn** softmod.
|
||||
|
||||
### Installing with Sony Utility Discs Compilation 3
|
||||
|
||||
@ -158,5 +239,8 @@ Before installing the English patch, you **must** power off your console to stan
|
||||
|
||||
---
|
||||
|
||||
\* Instances in feega where some Japanese text could not be translated because it is hard coded, most likely in an encrypted file. Atok software has not been translated. You might have to manually change the title of your "Favorite" folders if they were created before running this patch.
|
||||
\** **PS2 HDD RAW Image Install** - not compatible with early model Japanese PS2 consoles that have an external HDD due to space limitations. **Patch an existing PSBBN install** - Kloader might have compatibility issues with early model Japanese PS2 consoles.
|
||||
</details>
|
||||
|
||||
|
||||
\* Instances in feega where some Japanese text could not be translated because it is hard coded in an encrypted file. Atok software has not been translated. You might have to manually change the title of your "Favorite" folders if they were created before you **Patch an existing PSBBN install**.
|
||||
\** PSBBN Definitive English Patch 2.0 and older versions of the **PS2 HDD RAW Image Install** - not compatible with early model Japanese PS2 consoles that have an external HDD due to space limitations. **Patch an existing PSBBN install** - Kloader might have compatibility issues with early model Japanese PS2 consoles.
|
||||
|
BIN
assets/IGR_BG.TM2
Normal file
BIN
assets/IGR_NO.TM2
Normal file
BIN
assets/IGR_YES.TM2
Normal file
BIN
assets/OPNPS2LD.ELF
Normal file
BIN
assets/POPSTARTER.ELF
Normal file
18
assets/icon.sys
Normal file
@ -0,0 +1,18 @@
|
||||
PS2X
|
||||
title0=OPL Launcher BDM
|
||||
title1=
|
||||
bgcola=0
|
||||
bgcol0=0,0,0
|
||||
bgcol1=0,0,0
|
||||
bgcol2=0,0,0
|
||||
bgcol3=0,0,0
|
||||
lightdir0=1.0,-1.0,1.0
|
||||
lightdir1=-1.0,1.0,-1.0
|
||||
lightdir2=0.0,0.0,0.0
|
||||
lightcolamb=64,64,64
|
||||
lightcol0=64,64,64
|
||||
lightcol1=16,16,16
|
||||
lightcol2=0,0,0
|
||||
uninstallmes0=
|
||||
uninstallmes1=
|
||||
uninstallmes2=
|
BIN
assets/list.ico
Normal file
After Width: | Height: | Size: 21 KiB |
4
assets/system.cnf
Normal file
@ -0,0 +1,4 @@
|
||||
BOOT2 = pfs:/OPL-Launcher-BDM.KELF
|
||||
VER = 1.01
|
||||
VMODE = NTSC
|
||||
HDDUNITPOWER = NICHDD
|
22584
helper/ArtDB.csv
Normal file
BIN
helper/HDL Dump.elf
Executable file
BIN
helper/PFS Shell.elf
Executable file
BIN
helper/PS2 APA Header Checksum Fixer.elf
Executable file
11030
helper/TitlesDB_PS1_English.csv
Normal file
11902
helper/TitlesDB_PS2_English.csv
Normal file
100
helper/art_downloader.js
Normal file
@ -0,0 +1,100 @@
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Check if Puppeteer is installed, and install it if not
|
||||
try {
|
||||
require.resolve('puppeteer');
|
||||
} catch (e) {
|
||||
console.log("Puppeteer not found. Installing...");
|
||||
try {
|
||||
execSync('npm install puppeteer', { stdio: 'inherit' });
|
||||
console.log("Puppeteer installed successfully.");
|
||||
} catch (installError) {
|
||||
console.error("Failed to install Puppeteer. Ensure you have npm installed and try again.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const puppeteer = require('puppeteer'); // Import Puppeteer after ensuring it's installed
|
||||
|
||||
(async () => {
|
||||
// Get the game ID from the command-line arguments
|
||||
const gameId = process.argv[2];
|
||||
|
||||
if (!gameId) {
|
||||
console.error("Usage: node script.js <gameid>");
|
||||
process.exit(1); // Exit if no game ID is provided
|
||||
}
|
||||
|
||||
const csvFilePath = './helper/ArtDB.csv';
|
||||
const outputDir = './icons/art/tmp';
|
||||
|
||||
// Ensure the output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Function to search the CSV file for the game ID
|
||||
const findUrlForGameId = async (gameId) => {
|
||||
const fileStream = fs.createReadStream(csvFilePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
const [id, urlPart] = line.split('|');
|
||||
if (id === gameId) {
|
||||
return `https://www.ign.com/games/${urlPart}`; // Construct the full URL
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Return null if no match is found
|
||||
};
|
||||
|
||||
const url = await findUrlForGameId(gameId);
|
||||
|
||||
if (!url) {
|
||||
console.error(`Game ID "${gameId}" not found in ArtDB.csv`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
console.log(`Navigating to: ${url}`);
|
||||
await page.goto(url, { waitUntil: 'networkidle2' });
|
||||
|
||||
// Find the first image with src starting with 'https://assets-prd.ignimgs.com'
|
||||
const imgUrl = await page.evaluate(() => {
|
||||
const img = document.querySelector('img[src^="https://assets-prd.ignimgs.com"]'); // Look for images with this src prefix
|
||||
|
||||
if (img) {
|
||||
return img.src.split('?')[0]; // Remove query parameters
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (imgUrl) {
|
||||
// Get the file extension from the URL
|
||||
const fileExtension = path.extname(imgUrl).split('?')[0]; // Ensures query strings don't interfere
|
||||
|
||||
// Save the image in the specified directory with the correct file extension
|
||||
const fileName = path.join(outputDir, `${gameId}${fileExtension}`);
|
||||
console.log(`Downloading image from: ${imgUrl}`);
|
||||
console.log(`Saving as: ${fileName}`);
|
||||
const file = fs.createWriteStream(fileName);
|
||||
https.get(imgUrl, (response) => response.pipe(file));
|
||||
} else {
|
||||
console.log("No image found with the specified source.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch the page: ${error.message}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
236
helper/list-builder-ps1.py
Normal file
@ -0,0 +1,236 @@
|
||||
import os.path
|
||||
import sys
|
||||
import subprocess
|
||||
import math
|
||||
import unicodedata
|
||||
from natsort import natsorted
|
||||
|
||||
done = "Error: No games found."
|
||||
total = 0
|
||||
count = 0
|
||||
pattern_1 = [b'\x01', b'\x0D']
|
||||
pattern_2 = [b'\x3B', b'\x31']
|
||||
|
||||
# Function to count VCD files in the POPS folder
|
||||
def count_vcd(folder):
|
||||
global total
|
||||
for image in os.listdir(game_path + folder):
|
||||
if image.lower().endswith(".vcd"):
|
||||
total += 1
|
||||
|
||||
# Function to process VCD files in the POPS folder
|
||||
def process_vcd(folder):
|
||||
global total
|
||||
global count
|
||||
global done
|
||||
|
||||
gameid_file_path = "./helper/TitlesDB_PS1_English.csv"
|
||||
|
||||
# Read TitlesDB_PS1_English.csv and create a dictionary of title IDs to game names
|
||||
game_names = {}
|
||||
if os.path.isfile(gameid_file_path):
|
||||
with open(gameid_file_path, 'r') as gameid_file:
|
||||
for line in gameid_file:
|
||||
parts = line.strip().split('|') # Split title ID and game name
|
||||
if len(parts) >= 3:
|
||||
game_names[parts[0]] = (parts[1], parts[2])
|
||||
|
||||
# Prepare a list to hold all game list entries
|
||||
game_list_entries = []
|
||||
|
||||
for image in os.listdir(game_path + folder):
|
||||
if image.lower().endswith(".vcd"):
|
||||
print(math.floor((count * 100) / total), '% complete')
|
||||
print('Processing', image)
|
||||
index = 0
|
||||
string = ""
|
||||
|
||||
with open(game_path + folder + "/" + image, "rb") as file:
|
||||
while (byte := file.read(1)):
|
||||
if len(string) < 4:
|
||||
if index == 2:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif byte == pattern_1[index]:
|
||||
index += 1
|
||||
else:
|
||||
string = ""
|
||||
index = 0
|
||||
elif len(string) == 4:
|
||||
index = 0
|
||||
if byte == b'\x5F':
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
else:
|
||||
string = ""
|
||||
elif len(string) < 8:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif len(string) == 8:
|
||||
if byte == b'\x2E':
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
else:
|
||||
string = ""
|
||||
elif len(string) < 11:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif len(string) == 11:
|
||||
if byte == pattern_2[index]:
|
||||
index += 1
|
||||
if index == 2:
|
||||
break
|
||||
else:
|
||||
string = ""
|
||||
index = 0
|
||||
|
||||
count += 1
|
||||
|
||||
# If no title ID is found, set it to the first 11 characters of the filename
|
||||
if len(string) != 11:
|
||||
string = os.path.splitext(image)[0][:11]
|
||||
print(f'No title ID found. Defaulting to first 11 chars of filename: {string}')
|
||||
|
||||
# Determine game name and publisher
|
||||
entry = game_names.get(string)
|
||||
|
||||
if entry:
|
||||
# If we found a match in the CSV
|
||||
game_name = entry[0] if entry[0] else None # If game name is empty, set to None
|
||||
publisher = entry[1] if len(entry) > 1 and entry[1] else ""
|
||||
if not game_name: # If game name is None (i.e., found in CSV but empty)
|
||||
print(f"Game ID '{string}' found in CSV, but title is missing. Using filename logic.")
|
||||
file_name_without_ext = os.path.splitext(image)[0]
|
||||
if len(file_name_without_ext) >= 12 and file_name_without_ext[4] == '_' and file_name_without_ext[8] == '.' and file_name_without_ext[11] == '.':
|
||||
game_name = file_name_without_ext[12:] # Fallback to part after the game ID
|
||||
else:
|
||||
game_name = file_name_without_ext # Use the filename as-is
|
||||
publisher = "" # Publisher will remain empty in this case
|
||||
print(f"Match found: ID='{string}' -> Game='{game_name}', Publisher='{publisher}'")
|
||||
else:
|
||||
# If no match found in CSV, use filename logic for game name
|
||||
print(f"No match found for ID='{string}'")
|
||||
file_name_without_ext = os.path.splitext(image)[0]
|
||||
if len(file_name_without_ext) >= 12 and file_name_without_ext[4] == '_' and file_name_without_ext[8] == '.' and file_name_without_ext[11] == '.':
|
||||
game_name = file_name_without_ext[12:]
|
||||
else:
|
||||
game_name = file_name_without_ext
|
||||
publisher = ""
|
||||
print(f"Default game name from filename: '{game_name}'")
|
||||
|
||||
# Format entry with game name, game ID, publisher, and image info
|
||||
folder_image = f"{folder.replace('/', '', 1)}|{image}"
|
||||
game_list_entry = f"{game_name}|{string}|{publisher}|{folder_image}"
|
||||
game_list_entries.append(game_list_entry)
|
||||
|
||||
if game_list_entries:
|
||||
with open(os.path.join(game_path, 'ps1.list'), "a") as output:
|
||||
for entry in game_list_entries:
|
||||
output.write(f"{entry}\n")
|
||||
|
||||
done = "Done!"
|
||||
|
||||
# Function to normalize text by removing diacritical marks and converting to ASCII
|
||||
def normalize_text(text):
|
||||
"""
|
||||
Normalize text by removing diacritical marks and converting to ASCII.
|
||||
"""
|
||||
return ''.join(
|
||||
c for c in unicodedata.normalize('NFD', text)
|
||||
if unicodedata.category(c) != 'Mn'
|
||||
)
|
||||
|
||||
# Main function to sort the games list
|
||||
def sort_games_list(game_path):
|
||||
games_list_path = os.path.join(game_path, 'ps1.list')
|
||||
|
||||
# Read the ps1.list into a list of lines
|
||||
with open(games_list_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
# Sort the lines by the first field dynamically
|
||||
def sort_key(line):
|
||||
# Split the line into fields
|
||||
fields = line.strip().split('|')
|
||||
|
||||
# Extract the game title (first field) and game_id (second field, if present)
|
||||
first_field = fields[0].strip()
|
||||
game_id = fields[1].strip() if len(fields) > 1 else ""
|
||||
|
||||
# Check for colon and truncate at the first colon, if exists
|
||||
if ':' in first_field:
|
||||
first_field = first_field.split(':')[0].strip()
|
||||
|
||||
# Remove leading "The" or "the" for sorting purposes
|
||||
if first_field.lower().startswith('the '):
|
||||
first_field = first_field[4:].strip()
|
||||
|
||||
# Normalize the title
|
||||
normalized_title = normalize_text(first_field)
|
||||
|
||||
# Check for special cases like Roman numeral endings
|
||||
replacements = {
|
||||
' II': ' 2',
|
||||
' III': ' 3',
|
||||
' IV': ' 4',
|
||||
' V': ' 5',
|
||||
' VI': ' 6',
|
||||
' VII': ' 7',
|
||||
' VIII': ' 8',
|
||||
' IX': ' 9',
|
||||
' X': ' 10',
|
||||
' XI': ' 11',
|
||||
' XII': ' 12',
|
||||
' XIII': ' 13',
|
||||
' XIV': ' 14',
|
||||
' XV': ' 15',
|
||||
' XVI': ' 16',
|
||||
' XVII': ' 17',
|
||||
' XVIII': ' 18',
|
||||
' XIX': ' 19',
|
||||
' XX': ' 20'
|
||||
}
|
||||
for roman, digit in replacements.items():
|
||||
if normalized_title.endswith(roman):
|
||||
normalized_title = normalized_title.replace(roman, digit)
|
||||
break
|
||||
|
||||
final_key = normalized_title.lower()
|
||||
return (final_key, game_id)
|
||||
|
||||
# Sort the lines by the dynamic key using natsorted
|
||||
sorted_lines = natsorted(lines, key=sort_key)
|
||||
|
||||
# Write the sorted lines back to ps1.list
|
||||
with open(games_list_path, 'w') as file:
|
||||
file.writelines(sorted_lines)
|
||||
|
||||
def main(arg1):
|
||||
if arg1:
|
||||
global game_path
|
||||
global current_dir
|
||||
game_path = arg1
|
||||
current_dir = os.getcwd()
|
||||
|
||||
# Remove any existing game list file
|
||||
ps1_list_path = os.path.join(game_path, 'ps1.list')
|
||||
if os.path.isfile(ps1_list_path):
|
||||
os.remove(ps1_list_path)
|
||||
|
||||
# Count and process files in the DVD and CD folders
|
||||
if os.path.isdir(game_path + '/POPS'):
|
||||
count_vcd('/POPS')
|
||||
if total == 0: # No VCD files found
|
||||
print("No PS1 games found in the POPS folder.")
|
||||
sys.exit(1)
|
||||
process_vcd('/POPS')
|
||||
else:
|
||||
print('POPS folder not found at ' + game_path)
|
||||
sys.exit(1)
|
||||
|
||||
# Sort the games list after processing
|
||||
sort_games_list(game_path)
|
||||
|
||||
print(done)
|
||||
print('')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print('Usage: python3 list_builder-ps1.py <path/to/games>')
|
||||
sys.exit(1)
|
||||
main(sys.argv[1])
|
300
helper/list-builder-ps2.py
Normal file
@ -0,0 +1,300 @@
|
||||
import sys
|
||||
import math
|
||||
import os.path
|
||||
import subprocess
|
||||
import unicodedata
|
||||
from natsort import natsorted
|
||||
|
||||
done = "Error: No games found."
|
||||
total = 0
|
||||
count = 0
|
||||
pattern_1 = [b'\x01', b'\x0D']
|
||||
pattern_2 = [b'\x3B', b'\x31']
|
||||
|
||||
# Function to count game files in the given folder
|
||||
def count_files(folder, extensions):
|
||||
global total
|
||||
for image in os.listdir(game_path + folder):
|
||||
if any(image.lower().endswith(ext) for ext in extensions):
|
||||
total += 1
|
||||
|
||||
# Function to process game files in the given folder
|
||||
def process_files(folder, extensions):
|
||||
global total
|
||||
global count
|
||||
global done
|
||||
|
||||
gameid_file_path = "./helper/TitlesDB_PS2_English.csv"
|
||||
|
||||
# Read TitlesDB_PS2_English.csv and create a dictionary of title IDs to game names
|
||||
game_names = {}
|
||||
if os.path.isfile(gameid_file_path):
|
||||
with open(gameid_file_path, 'r') as gameid_file:
|
||||
for line in gameid_file:
|
||||
parts = line.strip().split('|') # Split title ID and game name
|
||||
if len(parts) == 3:
|
||||
game_names[parts[0]] = (parts[1], parts[2])
|
||||
|
||||
# Prepare a list to hold all game list entries
|
||||
game_list_entries = []
|
||||
|
||||
for image in os.listdir(game_path + folder):
|
||||
if any(image.lower().endswith(ext) for ext in extensions):
|
||||
print(math.floor((count * 100) / total), '% complete')
|
||||
print('Processing', image)
|
||||
index = 0
|
||||
string = ""
|
||||
|
||||
original_image = image # Store the original filename (e.g., `.zso` or `.iso`)
|
||||
converted_iso = False
|
||||
|
||||
|
||||
# Check the filename condition for all files
|
||||
file_name_without_ext = os.path.splitext(image)[0]
|
||||
if len(file_name_without_ext) >= 9 and file_name_without_ext[4] == '_' and file_name_without_ext[8] == '.':
|
||||
# Filename meets the condition, directly set the game ID
|
||||
string = file_name_without_ext[:11]
|
||||
print(f"Filename meets condition. Game ID set directly from filename: {string}")
|
||||
|
||||
# If the file has a .zso extension and no ID was set, convert to .iso
|
||||
if image.lower().endswith('.zso') and not string:
|
||||
zso_path = os.path.join(game_path + folder, image)
|
||||
iso_path = os.path.join(game_path + folder, os.path.splitext(image)[0] + '.iso')
|
||||
|
||||
print(f"Converting {image} from .zso to .iso...")
|
||||
venv_activate = os.path.join('venv', 'bin', 'activate')
|
||||
command = f"source {venv_activate} && python3 ./helper/ziso.py -c 0 '{zso_path}' '{iso_path}'"
|
||||
subprocess.run(command, shell=True, check=True, executable='/bin/bash')
|
||||
|
||||
# Update image to the new .iso path for processing
|
||||
image = os.path.basename(iso_path)
|
||||
converted_iso = True # Mark the .iso file as being converted from .zso
|
||||
|
||||
# Extract the game ID from the file content if not set by the filename
|
||||
if not string: # Only process if the game ID is not set from the filename
|
||||
with open(game_path + folder + "/" + image, "rb") as file:
|
||||
while (byte := file.read(1)):
|
||||
if len(string) < 4:
|
||||
if index == 2:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif byte == pattern_1[index]:
|
||||
index += 1
|
||||
else:
|
||||
string = ""
|
||||
index = 0
|
||||
elif len(string) == 4:
|
||||
index = 0
|
||||
if byte == b'\x5F':
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
else:
|
||||
string = ""
|
||||
elif len(string) < 8:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif len(string) == 8:
|
||||
if byte == b'\x2E':
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
else:
|
||||
string = ""
|
||||
elif len(string) < 11:
|
||||
string += byte.decode('utf-8', errors='ignore')
|
||||
elif len(string) == 11:
|
||||
if byte == pattern_2[index]:
|
||||
index += 1
|
||||
if index == 2:
|
||||
break
|
||||
else:
|
||||
string = ""
|
||||
index = 0
|
||||
|
||||
count += 1
|
||||
|
||||
# Fallback if no title ID is found
|
||||
if len(string) != 11:
|
||||
string = os.path.splitext(original_image)[0][:11]
|
||||
print(f'No title ID found. Defaulting to first 11 chars of filename: {string}')
|
||||
|
||||
# Rename the original `.zso` file to begin with the `gameid`
|
||||
if converted_iso:
|
||||
new_filename = f"{string}.{original_image}"
|
||||
new_zso_path = os.path.join(game_path + folder, new_filename)
|
||||
os.rename(zso_path, new_zso_path)
|
||||
print(f"Renamed {original_image} to {new_filename}")
|
||||
original_image = new_filename # Update the original image reference
|
||||
|
||||
# Determine game name and publisher
|
||||
entry = game_names.get(string)
|
||||
if entry:
|
||||
game_name, publisher = entry
|
||||
else:
|
||||
game_name = os.path.splitext(original_image)[0]
|
||||
publisher = ""
|
||||
|
||||
# Format entry with game name, game ID, publisher, and updated original image info
|
||||
folder_image = f"{folder.replace('/', '', 1)}|{original_image}"
|
||||
game_list_entry = f"{game_name}|{string}|{publisher}|{folder_image}"
|
||||
game_list_entries.append(game_list_entry)
|
||||
|
||||
# If the file was converted from .zso to .iso, delete the .iso file
|
||||
if converted_iso:
|
||||
os.remove(game_path + folder + "/" + image)
|
||||
print(f"Deleted the temporary ISO file: {image}")
|
||||
|
||||
# Write all entries to the ps2.list file
|
||||
if game_list_entries:
|
||||
with open(os.path.join(game_path, 'ps2.list'), "a") as output:
|
||||
for entry in game_list_entries:
|
||||
output.write(f"{entry}\n")
|
||||
|
||||
done = "Done!"
|
||||
|
||||
# Function to normalize text by removing diacritical marks and converting to ASCII
|
||||
def normalize_text(text):
|
||||
"""
|
||||
Normalize text by removing diacritical marks and converting to ASCII.
|
||||
"""
|
||||
return ''.join(
|
||||
c for c in unicodedata.normalize('NFD', text)
|
||||
if unicodedata.category(c) != 'Mn'
|
||||
)
|
||||
|
||||
# Main function to sort the games list
|
||||
def sort_games_list(game_path):
|
||||
games_list_path = os.path.join(game_path, 'ps2.list')
|
||||
|
||||
# Read the ps2.list into a list of lines
|
||||
with open(games_list_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
# Sort the lines by the first field dynamically
|
||||
def sort_key(line):
|
||||
# Split the line into fields
|
||||
fields = line.strip().split('|')
|
||||
|
||||
# Extract the game title (first field) and game_id (second field, if present)
|
||||
first_field = fields[0].strip()
|
||||
game_id = fields[1].strip() if len(fields) > 1 else ""
|
||||
|
||||
# Special condition for 'Jak and Daxter: The Precursor Legacy'
|
||||
if first_field.lower() == "jak and daxter: the precursor legacy":
|
||||
return ("jak", game_id)
|
||||
|
||||
# Special condition for 'Ratchet: Deadlocked'
|
||||
if first_field.lower() == "ratchet: deadlocked":
|
||||
return ("ratchet & clank", game_id)
|
||||
|
||||
# Special condition for 'Ratchet: Deadlocked'
|
||||
if first_field.lower() == "secret agent clank":
|
||||
return ("ratchet & clank", game_id)
|
||||
|
||||
# Special condition for 'Sly Cooper and the Thievius Raccoonus'
|
||||
if first_field.lower().startswith("sly cooper and the thievius raccoonus"):
|
||||
return ("sly", game_id)
|
||||
|
||||
# Special condition for 'Zone of the Enders: The 2nd Runner'
|
||||
if first_field.lower().startswith("zone of the enders: the 2nd runner"):
|
||||
return ("zone of the enders 2", game_id)
|
||||
|
||||
# Special condition for 'Grand Theft Auto III'
|
||||
if first_field.lower().startswith("grand theft auto iii"):
|
||||
return ("grand theft auto", game_id)
|
||||
|
||||
# Special condition for 'The Document of Metal Gear Solid 2'
|
||||
if first_field.lower().startswith("the document of metal gear solid 2"):
|
||||
return ("metal gear solid 2", game_id)
|
||||
|
||||
# Special condition for 'Forbidden Siren'
|
||||
if first_field.lower().startswith("forbidden siren 2"):
|
||||
return ("siren 2", game_id)
|
||||
|
||||
# Special condition for 'We Love Katamari'
|
||||
if first_field.lower().startswith("we love katamari"):
|
||||
return ("katamari damacy 2", game_id)
|
||||
|
||||
# Check for colon and truncate at the first colon, if exists
|
||||
if ':' in first_field:
|
||||
first_field = first_field.split(':')[0].strip()
|
||||
|
||||
# Remove leading "The" or "the" for sorting purposes
|
||||
if first_field.lower().startswith('the '):
|
||||
first_field = first_field[4:].strip()
|
||||
|
||||
# Normalize the title
|
||||
normalized_title = normalize_text(first_field)
|
||||
|
||||
# Check for special cases like Roman numeral endings
|
||||
replacements = {
|
||||
' II': ' 2',
|
||||
' III': ' 3',
|
||||
' IV': ' 4',
|
||||
' V': ' 5',
|
||||
' VI': ' 6',
|
||||
' VII': ' 7',
|
||||
' VIII': ' 8',
|
||||
' IX': ' 9',
|
||||
' X': ' 10',
|
||||
' XI': ' 11',
|
||||
' XII': ' 12',
|
||||
' XIII': ' 13',
|
||||
' XIV': ' 14',
|
||||
' XV': ' 15',
|
||||
' XVI': ' 16',
|
||||
' XVII': ' 17',
|
||||
' XVIII': ' 18',
|
||||
' XIX': ' 19',
|
||||
' XX': ' 20'
|
||||
}
|
||||
for roman, digit in replacements.items():
|
||||
if normalized_title.endswith(roman):
|
||||
normalized_title = normalized_title.replace(roman, digit)
|
||||
break
|
||||
|
||||
final_key = normalized_title.lower()
|
||||
return (final_key, game_id)
|
||||
|
||||
# Sort the lines by the dynamic key using natsorted
|
||||
sorted_lines = natsorted(lines, key=sort_key)
|
||||
|
||||
# Write the sorted lines back to ps2.list
|
||||
with open(games_list_path, 'w') as file:
|
||||
file.writelines(sorted_lines)
|
||||
|
||||
def main(arg1):
|
||||
global game_path
|
||||
global current_dir
|
||||
game_path = arg1
|
||||
current_dir = os.getcwd()
|
||||
|
||||
# Remove any existing game list file
|
||||
ps2_list_path = os.path.join(game_path, 'ps2.list')
|
||||
if os.path.isfile(ps2_list_path):
|
||||
os.remove(ps2_list_path)
|
||||
|
||||
# Count and process files in the DVD and CD folders
|
||||
for folder, extensions in [('/DVD', ['.iso', '.zso']), ('/CD', ['.iso', '.zso'])]:
|
||||
if os.path.isdir(game_path + folder):
|
||||
count_files(folder, extensions)
|
||||
else:
|
||||
print(f'{folder} not found at ' + game_path)
|
||||
sys.exit(1)
|
||||
|
||||
# Check if no games were found
|
||||
if total == 0:
|
||||
print("No PS2 games found in the CD or DVD folder.")
|
||||
sys.exit(1)
|
||||
|
||||
# Process the files now that we know there are games
|
||||
for folder, extensions in [('/DVD', ['.iso', '.zso']), ('/CD', ['.iso', '.zso'])]:
|
||||
if os.path.isdir(game_path + folder):
|
||||
process_files(folder, extensions)
|
||||
|
||||
# Sort the games list after processing
|
||||
sort_games_list(game_path)
|
||||
|
||||
print(done)
|
||||
print('')
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print('Usage: list_builder-ps2.py <path/to/games>')
|
||||
sys.exit(1)
|
||||
main(sys.argv[1])
|
424
helper/ziso.py
Normal file
@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2011 by Virtuous Flame
|
||||
# Based BOOSTER 1.01 CSO Compressor
|
||||
# Adapted for codestation's ZSO format
|
||||
#
|
||||
# GNU General Public Licence (GPL)
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
|
||||
# Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
|
||||
__author__ = "Virtuous Flame"
|
||||
__license__ = "GPL"
|
||||
__version__ = "2.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
import lz4.block
|
||||
from struct import pack, unpack
|
||||
from multiprocessing import Pool
|
||||
from getopt import gnu_getopt, GetoptError
|
||||
|
||||
ZISO_MAGIC = 0x4F53495A
|
||||
DEFAULT_ALIGN = 0
|
||||
DEFAULT_BLOCK_SIZE = 0x800
|
||||
COMPRESS_THREHOLD = 95
|
||||
DEFAULT_PADDING = br'X'
|
||||
|
||||
MP = False
|
||||
MP_NR = 1024 * 16
|
||||
|
||||
|
||||
def hexdump(data):
|
||||
for i in data:
|
||||
print("0x%02X" % ((ord(i))))
|
||||
print("")
|
||||
|
||||
|
||||
def lz4_compress(plain, level=9):
|
||||
mode = "high_compression" if level > 1 else "default"
|
||||
return lz4.block.compress(plain, mode=mode, compression=level, store_size=False)
|
||||
|
||||
|
||||
def lz4_compress_mp(i):
|
||||
plain = i[0]
|
||||
level = i[1]
|
||||
mode = "high_compression" if level > 1 else "default"
|
||||
return lz4.block.compress(plain, mode=mode, compression=level, store_size=False)
|
||||
|
||||
|
||||
def lz4_decompress(compressed, block_size):
|
||||
decompressed = None
|
||||
while True:
|
||||
try:
|
||||
decompressed = lz4.block.decompress(
|
||||
compressed, uncompressed_size=block_size)
|
||||
break
|
||||
except lz4.block.LZ4BlockError:
|
||||
compressed = compressed[:-1]
|
||||
return decompressed
|
||||
|
||||
|
||||
def usage():
|
||||
print("Usage: ziso [-c level] [-m] [-t percent] [-h] infile outfile")
|
||||
print(" -c level: 1-12 compress ISO to ZSO, 1 for standard compression, >1 for high compression")
|
||||
print(" 0 decompress ZSO to ISO")
|
||||
print(" -b size: 2048-8192, specify block size (2048 by default)")
|
||||
print(" -m Use multiprocessing acceleration for compressing")
|
||||
print(" -t percent Compression Threshold (1-100)")
|
||||
print(" -a align Padding alignment 0=small/slow 6=fast/large")
|
||||
print(" -p pad Padding byte")
|
||||
print(" -h this help")
|
||||
|
||||
|
||||
def open_input_output(fname_in, fname_out):
|
||||
try:
|
||||
fin = open(fname_in, "rb")
|
||||
except IOError:
|
||||
print("Can't open %s" % (fname_in))
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
fout = open(fname_out, "wb")
|
||||
except IOError:
|
||||
print("Can't create %s" % (fname_out))
|
||||
sys.exit(-1)
|
||||
|
||||
return fin, fout
|
||||
|
||||
|
||||
def seek_and_read(fin, offset, size):
|
||||
fin.seek(offset)
|
||||
return fin.read(size)
|
||||
|
||||
|
||||
def read_zso_header(fin):
|
||||
# ZSO header has 0x18 bytes
|
||||
data = seek_and_read(fin, 0, 0x18)
|
||||
magic, header_size, total_bytes, block_size, ver, align = unpack(
|
||||
'IIQIbbxx', data)
|
||||
return magic, header_size, total_bytes, block_size, ver, align
|
||||
|
||||
|
||||
def generate_zso_header(magic, header_size, total_bytes, block_size, ver, align):
|
||||
data = pack('IIQIbbxx', magic, header_size,
|
||||
total_bytes, block_size, ver, align)
|
||||
return data
|
||||
|
||||
|
||||
def show_zso_info(fname_in, fname_out, total_bytes, block_size, total_block, ver, align):
|
||||
print("Decompress '%s' to '%s'" % (fname_in, fname_out))
|
||||
print("Total File Size %ld bytes" % (total_bytes))
|
||||
print("block size %d bytes" % (block_size))
|
||||
print("total blocks %d blocks" % (total_block))
|
||||
print("index align %d" % (align))
|
||||
print("version %d" % (ver))
|
||||
|
||||
|
||||
def decompress_zso(fname_in, fname_out):
|
||||
fin, fout = open_input_output(fname_in, fname_out)
|
||||
magic, header_size, total_bytes, block_size, ver, align = read_zso_header(
|
||||
fin)
|
||||
|
||||
if magic != ZISO_MAGIC or block_size == 0 or total_bytes == 0 or header_size != 24 or ver > 1:
|
||||
print("ziso file format error")
|
||||
return -1
|
||||
|
||||
total_block = total_bytes // block_size
|
||||
index_buf = []
|
||||
|
||||
for _ in range(total_block + 1):
|
||||
index_buf.append(unpack('I', fin.read(4))[0])
|
||||
|
||||
show_zso_info(fname_in, fname_out, total_bytes,
|
||||
block_size, total_block, ver, align)
|
||||
|
||||
block = 0
|
||||
percent_period = total_block/100
|
||||
percent_cnt = 0
|
||||
|
||||
while block < total_block:
|
||||
percent_cnt += 1
|
||||
if percent_cnt >= percent_period and percent_period != 0:
|
||||
percent_cnt = 0
|
||||
print("decompress %d%%\r" %
|
||||
(block / percent_period), file=sys.stderr, end='\r')
|
||||
|
||||
index = index_buf[block]
|
||||
plain = index & 0x80000000
|
||||
index &= 0x7fffffff
|
||||
read_pos = index << (align)
|
||||
|
||||
if plain:
|
||||
read_size = block_size
|
||||
else:
|
||||
index2 = index_buf[block+1] & 0x7fffffff
|
||||
# Have to read more bytes if align was set
|
||||
read_size = (index2-index) << (align)
|
||||
if block == total_block - 1:
|
||||
read_size = total_bytes - read_pos
|
||||
|
||||
zso_data = seek_and_read(fin, read_pos, read_size)
|
||||
|
||||
if plain:
|
||||
dec_data = zso_data
|
||||
else:
|
||||
try:
|
||||
dec_data = lz4_decompress(zso_data, block_size)
|
||||
|
||||
except Exception as e:
|
||||
print("%d block: 0x%08X %d %s" %
|
||||
(block, read_pos, read_size, e))
|
||||
sys.exit(-1)
|
||||
|
||||
if (len(dec_data) != block_size):
|
||||
print("%d block: 0x%08X %d" %
|
||||
(block, read_pos, read_size))
|
||||
sys.exit(-1)
|
||||
|
||||
fout.write(dec_data)
|
||||
block += 1
|
||||
|
||||
fin.close()
|
||||
fout.close()
|
||||
print("ziso decompress completed")
|
||||
|
||||
|
||||
def show_comp_info(fname_in, fname_out, total_bytes, block_size, ver, align, level):
|
||||
print("Compress '%s' to '%s'" % (fname_in, fname_out))
|
||||
print("Total File Size %ld bytes" % (total_bytes))
|
||||
print("block size %d bytes" % (block_size))
|
||||
print("index align %d" % (1 << align))
|
||||
print("compress level %d" % (level))
|
||||
print("version %d" % (ver))
|
||||
if MP:
|
||||
print("multiprocessing %s" % (MP))
|
||||
|
||||
|
||||
def set_align(fout, write_pos, align):
|
||||
if write_pos % (1 << align):
|
||||
align_len = (1 << align) - write_pos % (1 << align)
|
||||
fout.write(DEFAULT_PADDING * align_len)
|
||||
write_pos += align_len
|
||||
|
||||
return write_pos
|
||||
|
||||
|
||||
def compress_zso(fname_in, fname_out, level, bsize):
|
||||
fin, fout = open_input_output(fname_in, fname_out)
|
||||
fin.seek(0, os.SEEK_END)
|
||||
total_bytes = fin.tell()
|
||||
fin.seek(0)
|
||||
|
||||
magic, header_size, block_size, ver, align = ZISO_MAGIC, 0x18, bsize, 1, DEFAULT_ALIGN
|
||||
|
||||
# We have to use alignment on any ZSO files which > 2GB, for MSB bit of index as the plain indicator
|
||||
# If we don't then the index can be larger than 2GB, which its plain indicator was improperly set
|
||||
align = total_bytes // 2 ** 31
|
||||
|
||||
header = generate_zso_header(
|
||||
magic, header_size, total_bytes, block_size, ver, align)
|
||||
fout.write(header)
|
||||
|
||||
total_block = total_bytes // block_size
|
||||
index_buf = [0 for i in range(total_block + 1)]
|
||||
|
||||
fout.write(b"\x00\x00\x00\x00" * len(index_buf))
|
||||
show_comp_info(fname_in, fname_out, total_bytes, block_size, ver, align, level)
|
||||
|
||||
write_pos = fout.tell()
|
||||
percent_period = total_block/100
|
||||
percent_cnt = 0
|
||||
|
||||
if MP:
|
||||
pool = Pool()
|
||||
|
||||
block = 0
|
||||
while block < total_block:
|
||||
if MP:
|
||||
percent_cnt += min(total_block - block, MP_NR)
|
||||
else:
|
||||
percent_cnt += 1
|
||||
|
||||
if percent_cnt >= percent_period and percent_period != 0:
|
||||
percent_cnt = 0
|
||||
|
||||
if block == 0:
|
||||
print("compress %3d%% avarage rate %3d%%\r" % (
|
||||
block / percent_period, 0), file=sys.stderr, end='\r')
|
||||
else:
|
||||
print("compress %3d%% avarage rate %3d%%\r" % (
|
||||
block / percent_period, 100*write_pos/(block*block_size)), file=sys.stderr, end='\r')
|
||||
|
||||
if MP:
|
||||
iso_data = [(fin.read(block_size), level)
|
||||
for i in range(min(total_block - block, MP_NR))]
|
||||
zso_data_all = pool.map_async(
|
||||
lz4_compress_mp, iso_data).get(9999999)
|
||||
|
||||
for i, zso_data in enumerate(zso_data_all):
|
||||
write_pos = set_align(fout, write_pos, align)
|
||||
index_buf[block] = write_pos >> align
|
||||
|
||||
if 100 * len(zso_data) / len(iso_data[i][0]) >= min(COMPRESS_THREHOLD, 100):
|
||||
zso_data = iso_data[i][0]
|
||||
index_buf[block] |= 0x80000000 # Mark as plain
|
||||
elif index_buf[block] & 0x80000000:
|
||||
print(
|
||||
"Align error, you have to increase align by 1 or OPL won't be able to read offset above 2 ** 31 bytes")
|
||||
sys.exit(1)
|
||||
|
||||
fout.write(zso_data)
|
||||
write_pos += len(zso_data)
|
||||
block += 1
|
||||
else:
|
||||
iso_data = fin.read(block_size)
|
||||
|
||||
try:
|
||||
zso_data = lz4_compress(iso_data, level)
|
||||
except Exception as e:
|
||||
print("%d block: %s" % (block, e))
|
||||
sys.exit(-1)
|
||||
|
||||
write_pos = set_align(fout, write_pos, align)
|
||||
index_buf[block] = write_pos >> align
|
||||
|
||||
if 100 * len(zso_data) / len(iso_data) >= COMPRESS_THREHOLD:
|
||||
zso_data = iso_data
|
||||
index_buf[block] |= 0x80000000 # Mark as plain
|
||||
elif index_buf[block] & 0x80000000:
|
||||
print(
|
||||
"Align error, you have to increase align by 1 or CFW won't be able to read offset above 2 ** 31 bytes")
|
||||
sys.exit(1)
|
||||
|
||||
fout.write(zso_data)
|
||||
write_pos += len(zso_data)
|
||||
block += 1
|
||||
|
||||
# Last position (total size)
|
||||
index_buf[block] = write_pos >> align
|
||||
|
||||
# Update index block
|
||||
fout.seek(len(header))
|
||||
for i in index_buf:
|
||||
idx = pack('I', i)
|
||||
fout.write(idx)
|
||||
|
||||
print("ziso compress completed , total size = %8d bytes , rate %d%%" %
|
||||
(write_pos, (write_pos*100/total_bytes)))
|
||||
|
||||
fin.close()
|
||||
fout.close()
|
||||
|
||||
|
||||
def parse_args():
|
||||
global MP, COMPRESS_THREHOLD, DEFAULT_PADDING, DEFAULT_ALIGN
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
usage()
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
optlist, args = gnu_getopt(sys.argv, "c:b:mt:a:p:h")
|
||||
except GetoptError as err:
|
||||
print(str(err))
|
||||
usage()
|
||||
sys.exit(-1)
|
||||
|
||||
level = None
|
||||
bsize = DEFAULT_BLOCK_SIZE
|
||||
|
||||
for o, a in optlist:
|
||||
if o == '-c':
|
||||
level = int(a)
|
||||
elif o == '-b':
|
||||
bsize = int(a)
|
||||
elif o == '-m':
|
||||
MP = True
|
||||
elif o == '-t':
|
||||
COMPRESS_THREHOLD = min(int(a), 100)
|
||||
elif o == '-a':
|
||||
DEFAULT_ALIGN = int(a)
|
||||
elif o == '-p':
|
||||
DEFAULT_PADDING = bytes(a[0], encoding='utf8')
|
||||
elif o == '-h':
|
||||
usage()
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
fname_in, fname_out = args[1:3]
|
||||
except ValueError as err:
|
||||
print("You have to specify input/output filename: %s", err)
|
||||
sys.exit(-1)
|
||||
|
||||
if bsize%2048 != 0:
|
||||
print("Error, invalid block size. Must be multiple of 2048.")
|
||||
sys.exit(-1)
|
||||
|
||||
return level, bsize, fname_in, fname_out
|
||||
|
||||
|
||||
def load_sector_table(sector_table_fn, total_block, default_level=9):
|
||||
# In future we will support NC
|
||||
sectors = [default_level for i in range(total_block)]
|
||||
|
||||
with open(sector_table_fn) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
a = line.split(":")
|
||||
|
||||
if len(a) < 2:
|
||||
raise ValueError("Invalid line founded: %s" % (line))
|
||||
|
||||
if -1 == a[0].find("-"):
|
||||
try:
|
||||
sector, level = int(a[0]), int(a[1])
|
||||
except ValueError:
|
||||
raise ValueError("Invalid line founded: %s" % (line))
|
||||
if level < 1 or level > 9:
|
||||
raise ValueError("Invalid line founded: %s" % (line))
|
||||
sectors[sector] = level
|
||||
else:
|
||||
b = a[0].split("-")
|
||||
try:
|
||||
start, end, level = int(b[0]), int(b[1]), int(a[1])
|
||||
except ValueError:
|
||||
raise ValueError("Invalid line founded: %s" % (line))
|
||||
i = start
|
||||
while i < end:
|
||||
sectors[i] = level
|
||||
i += 1
|
||||
|
||||
return sectors
|
||||
|
||||
|
||||
def main():
|
||||
print("ziso-python %s by %s" % (__version__, __author__))
|
||||
level, bsize, fname_in, fname_out = parse_args()
|
||||
|
||||
if level == 0:
|
||||
decompress_zso(fname_in, fname_out)
|
||||
else:
|
||||
compress_zso(fname_in, fname_out, level, bsize)
|
||||
|
||||
|
||||
PROFILE = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
if PROFILE:
|
||||
import cProfile
|
||||
cProfile.run("main()")
|
||||
else:
|
||||
main()
|
BIN
icons/OPL-Launcher-BDM.KELF
Normal file
BIN
icons/art/PBPX_955.02.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/PBPX_955.03.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/PBPX_955.16.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
icons/art/PCPX_961.64.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
icons/art/SCES_500.34.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
icons/art/SCES_502.94.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/SCES_503.00.png
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
icons/art/SCES_507.60.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
icons/art/SCES_511.59.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
icons/art/SCES_538.51.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
icons/art/SCKA_200.95.png
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
icons/art/SCPS_100.87.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
icons/art/SCPS_150.09.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/SCPS_550.07.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/SCPS_720.01.png
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
icons/art/SCUS_941.03.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
icons/art/SCUS_941.83.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
icons/art/SCUS_941.94.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
icons/art/SCUS_944.23.png
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
icons/art/SCUS_944.48.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
icons/art/SCUS_944.49.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
icons/art/SCUS_971.02.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
icons/art/SCUS_971.05.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
icons/art/SCUS_971.11.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
icons/art/SCUS_971.15.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
icons/art/SCUS_971.24.png
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
icons/art/SCUS_971.25.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SCUS_971.40.png
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
icons/art/SCUS_971.42.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SCUS_971.67.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
icons/art/SCUS_971.98.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
icons/art/SCUS_971.99.png
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
icons/art/SCUS_972.10.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
icons/art/SCUS_972.65.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
icons/art/SCUS_972.68.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
icons/art/SCUS_973.16.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
icons/art/SCUS_973.28.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
icons/art/SCUS_973.30.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
icons/art/SCUS_973.53.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SCUS_973.55.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
icons/art/SCUS_973.99.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
icons/art/SCUS_974.02.png
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
icons/art/SCUS_974.08.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
icons/art/SCUS_974.64.png
Normal file
After Width: | Height: | Size: 133 KiB |
BIN
icons/art/SCUS_974.65.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
icons/art/SCUS_974.72.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
icons/art/SCUS_974.81.png
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
icons/art/SCUS_975.01.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
icons/art/SCUS_975.02.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
icons/art/SCUS_975.12.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
icons/art/SCUS_976.15.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
icons/art/SCUS_976.23.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
icons/art/SLES_502.47.png
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
icons/art/SLES_507.69.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
icons/art/SLES_511.13.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
icons/art/SLES_517.05.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
icons/art/SLES_520.96.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
icons/art/SLES_525.05.png
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
icons/art/SLES_527.19.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
icons/art/SLES_530.38.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SLES_530.73.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
icons/art/SLES_535.40.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
icons/art/SLES_541.86.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
icons/art/SLKA_250.07.png
Normal file
After Width: | Height: | Size: 135 KiB |
BIN
icons/art/SLKA_252.65.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SLPM_610.30.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
icons/art/SLPM_651.24.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
icons/art/SLPM_651.40.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
icons/art/SLPM_654.70.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
icons/art/SLPM_657.44.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
icons/art/SLPM_658.80.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
icons/art/SLPM_660.17.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
icons/art/SLPM_661.60.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
icons/art/SLPM_668.53.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
icons/art/SLPM_669.26.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
icons/art/SLPM_675.07.png
Normal file
After Width: | Height: | Size: 105 KiB |
BIN
icons/art/SLPM_742.42.png
Normal file
After Width: | Height: | Size: 109 KiB |