initial commit
This commit is contained in:
commit
ac1d9bb6b3
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
venv/
|
||||||
|
download_history.json
|
||||||
|
patches/
|
||||||
|
patched/
|
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# RomHack Races Downloader and Patcher
|
||||||
|
|
||||||
|
Scripts to download and patch Super Mario World ROM hacks from RomHack Races.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Python Dependencies
|
||||||
|
- Python 3.6+
|
||||||
|
- Required packages in `requirements.txt`
|
||||||
|
|
||||||
|
Install Python dependencies with:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Requirements
|
||||||
|
- [Flips](https://github.com/Alcaro/Flips) - ROM patching utility
|
||||||
|
- Original Super Mario World ROM (.smc format)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. Install Python dependencies
|
||||||
|
3. Ensure `flips` is installed and available in your PATH
|
||||||
|
4. Place your original Super Mario World ROM somewhere accessible
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Downloading Patches
|
||||||
|
|
||||||
|
The `downloader.py` script will download all ROM patches from RomHack Races and organize them by season:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python downloader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Patches will be downloaded to the `patches` directory, organized in season folders (Season1, Season2, etc.).
|
||||||
|
|
||||||
|
### Patching ROMs
|
||||||
|
|
||||||
|
The `patch_roms.sh` script applies the downloaded patches to your original ROM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show help
|
||||||
|
./patch_roms.sh
|
||||||
|
|
||||||
|
# Do a dry run (shows what would be patched without actually patching)
|
||||||
|
./patch_roms.sh --dry-run /path/to/original.smc
|
||||||
|
|
||||||
|
# Actually patch the ROMs
|
||||||
|
./patch_roms.sh /path/to/original.smc
|
||||||
|
```
|
||||||
|
|
||||||
|
Patched ROMs will be created in the `patched` directory with filenames like `Week001.smc`, `Week002.smc`, etc.
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
- Only patch ROMs that don't already exist in the output directory
|
||||||
|
- Show a summary of new and skipped patches
|
||||||
|
- Maintain correct week numbering from the original patches
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── downloader.py # Patch downloader script
|
||||||
|
├── patch_roms.sh # ROM patcher script
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── patches/ # Downloaded patches (created by downloader.py)
|
||||||
|
│ ├── Season1/
|
||||||
|
│ ├── Season2/
|
||||||
|
│ └── ...
|
||||||
|
└── patched/ # Patched ROMs (created by patch_roms.sh)
|
||||||
|
├── Week001.smc
|
||||||
|
├── Week002.smc
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To get new patches and ROMs:
|
||||||
|
|
||||||
|
1. Run the downloader:
|
||||||
|
```bash
|
||||||
|
python downloader.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Patch new ROMs:
|
||||||
|
```bash
|
||||||
|
./patch_roms.sh /path/to/original.smc
|
||||||
|
```
|
||||||
|
|
||||||
|
The scripts will only download new patches and create new ROMs that don't already exist.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The original ROM is not included and must be provided separately
|
||||||
|
- Patches are downloaded from RomHack Races (https://www.romhackraces.com/)
|
||||||
|
- ROMs are organized by week numbers for easy reference
|
232
downloader.py
Normal file
232
downloader.py
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class RomhackRaceScraper:
|
||||||
|
def __init__(self):
|
||||||
|
self.base_url = "https://www.romhackraces.com/levels.php"
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.rate_limit = 1
|
||||||
|
self.last_request = 0
|
||||||
|
self.download_history_file = "download_history.json"
|
||||||
|
self.download_history = self.load_download_history()
|
||||||
|
self.debug = True
|
||||||
|
|
||||||
|
os.makedirs('patches', exist_ok=True)
|
||||||
|
|
||||||
|
def debug_print(self, message):
|
||||||
|
if self.debug:
|
||||||
|
print(f"DEBUG: {message}")
|
||||||
|
|
||||||
|
def load_download_history(self):
|
||||||
|
try:
|
||||||
|
with open(self.download_history_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {
|
||||||
|
"last_update": "",
|
||||||
|
"downloaded_patches": {},
|
||||||
|
"last_season_checked": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def save_download_history(self):
|
||||||
|
with open(self.download_history_file, 'w') as f:
|
||||||
|
json.dump(self.download_history, f, indent=2)
|
||||||
|
|
||||||
|
def rate_limited_request(self, url):
|
||||||
|
self.debug_print(f"Making request to: {url}")
|
||||||
|
current_time = time.time()
|
||||||
|
time_since_last = current_time - self.last_request
|
||||||
|
if time_since_last < self.rate_limit:
|
||||||
|
time.sleep(self.rate_limit - time_since_last)
|
||||||
|
|
||||||
|
response = self.session.get(url)
|
||||||
|
self.last_request = time.time()
|
||||||
|
self.debug_print(f"Response status code: {response.status_code}")
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_week_number(self, element):
|
||||||
|
"""Extract week number from span element containing number images"""
|
||||||
|
# First find the span with font-size:18px that contains 'Week'
|
||||||
|
week_span = element.find('span', style='font-size:18px;')
|
||||||
|
if not week_span or 'Week' not in week_span.text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get all number images in this span
|
||||||
|
number_images = week_span.find_all('img')
|
||||||
|
if not number_images:
|
||||||
|
self.debug_print("Found week span but no number images")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract numbers from image filenames
|
||||||
|
numbers = [img['src'].split('/')[-1].split('.')[0] for img in number_images]
|
||||||
|
week_num = int(''.join(numbers))
|
||||||
|
self.debug_print(f"Found week number: {week_num}")
|
||||||
|
return week_num
|
||||||
|
except (ValueError, KeyError, IndexError) as e:
|
||||||
|
self.debug_print(f"Error parsing week number: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def download_patch(self, url, week_number, season_number):
|
||||||
|
patch_id = f"s{season_number}_w{week_number}"
|
||||||
|
|
||||||
|
if patch_id in self.download_history["downloaded_patches"]:
|
||||||
|
self.debug_print(f"Patch {patch_id} already downloaded")
|
||||||
|
return False
|
||||||
|
|
||||||
|
response = self.rate_limited_request(url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
season_dir = os.path.join('patches', f"Season{season_number}")
|
||||||
|
os.makedirs(season_dir, exist_ok=True)
|
||||||
|
|
||||||
|
filename = f"Week{week_number}.bps"
|
||||||
|
filepath = os.path.join(season_dir, filename)
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
self.download_history["downloaded_patches"][patch_id] = {
|
||||||
|
"filename": filepath,
|
||||||
|
"downloaded_at": datetime.now().isoformat(),
|
||||||
|
"url": url
|
||||||
|
}
|
||||||
|
self.save_download_history()
|
||||||
|
|
||||||
|
print(f"Downloaded Season {season_number} Week {week_number}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed to download Season {season_number} Week {week_number} - Status code: {response.status_code}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_seasons(self):
|
||||||
|
response = self.rate_limited_request(self.base_url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
season_div = soup.find('div', class_='info leaders', style=lambda s: s and '300px' in s)
|
||||||
|
if not season_div:
|
||||||
|
self.debug_print("Could not find season navigation div")
|
||||||
|
return []
|
||||||
|
|
||||||
|
season_links = season_div.find_all('a')
|
||||||
|
seasons = []
|
||||||
|
for link in season_links:
|
||||||
|
season_num = re.search(r'season=(\d+)', link['href'])
|
||||||
|
if season_num:
|
||||||
|
seasons.append(int(season_num.group(1)))
|
||||||
|
|
||||||
|
if 1 not in seasons:
|
||||||
|
seasons.append(1)
|
||||||
|
|
||||||
|
self.debug_print(f"Found seasons: {seasons}")
|
||||||
|
return sorted(seasons)
|
||||||
|
|
||||||
|
def test_parse(self):
|
||||||
|
"""Test parsing on the first page"""
|
||||||
|
response = self.rate_limited_request(self.base_url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Find first patch link
|
||||||
|
first_patch = soup.find('a', href=lambda href: href and href.endswith('.bps'))
|
||||||
|
if first_patch:
|
||||||
|
self.debug_print(f"Test found patch link: {first_patch['href']}")
|
||||||
|
else:
|
||||||
|
self.debug_print("Test could not find any patch links")
|
||||||
|
|
||||||
|
# Find first week span
|
||||||
|
first_week = soup.find('span', style='font-size:18px;')
|
||||||
|
if first_week:
|
||||||
|
self.debug_print(f"Test found week span: {first_week.text}")
|
||||||
|
number_images = first_week.find_all('img')
|
||||||
|
self.debug_print(f"Number images found: {len(number_images)}")
|
||||||
|
for img in number_images:
|
||||||
|
self.debug_print(f"Image source: {img['src']}")
|
||||||
|
else:
|
||||||
|
self.debug_print("Test could not find any week spans")
|
||||||
|
|
||||||
|
def scrape_season(self, season_number):
|
||||||
|
url = f"{self.base_url}?season={season_number}"
|
||||||
|
print(f"\nScraping Season {season_number}")
|
||||||
|
|
||||||
|
response = self.rate_limited_request(url)
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
downloads_this_season = 0
|
||||||
|
|
||||||
|
# Find all info divs
|
||||||
|
info_divs = soup.find_all('div', class_='info')
|
||||||
|
|
||||||
|
for info_div in info_divs:
|
||||||
|
# Check if this div contains a week number
|
||||||
|
week_num = self.get_week_number(info_div)
|
||||||
|
if week_num is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.debug_print(f"Processing Week {week_num}")
|
||||||
|
|
||||||
|
# Look for the patch link in the next table cell
|
||||||
|
table_cell = info_div.find_next('td', valign='top', align='right')
|
||||||
|
if table_cell:
|
||||||
|
patch_link = table_cell.find('a', href=lambda href: href and href.endswith('.bps'))
|
||||||
|
if patch_link:
|
||||||
|
self.debug_print(f"Found patch link: {patch_link['href']}")
|
||||||
|
patch_url = urljoin("https://www.romhackraces.com/", patch_link['href'])
|
||||||
|
self.debug_print(f"Full patch URL: {patch_url}")
|
||||||
|
|
||||||
|
if self.download_patch(patch_url, week_num, season_number):
|
||||||
|
downloads_this_season += 1
|
||||||
|
else:
|
||||||
|
self.debug_print(f"No patch link found for Week {week_num}")
|
||||||
|
else:
|
||||||
|
self.debug_print(f"No table cell found for Week {week_num}")
|
||||||
|
|
||||||
|
self.debug_print(f"Downloads this season: {downloads_this_season}")
|
||||||
|
return downloads_this_season
|
||||||
|
|
||||||
|
def scrape_all_seasons(self):
|
||||||
|
self.test_parse()
|
||||||
|
seasons = self.get_seasons()
|
||||||
|
print(f"Found {len(seasons)} seasons to scrape")
|
||||||
|
|
||||||
|
total_downloads = 0
|
||||||
|
last_season_checked = self.download_history["last_season_checked"]
|
||||||
|
|
||||||
|
for season in seasons:
|
||||||
|
if season < last_season_checked:
|
||||||
|
self.debug_print(f"Skipping Season {season} - already checked")
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloads = self.scrape_season(season)
|
||||||
|
total_downloads += downloads
|
||||||
|
|
||||||
|
self.download_history["last_season_checked"] = max(
|
||||||
|
season,
|
||||||
|
self.download_history["last_season_checked"]
|
||||||
|
)
|
||||||
|
self.download_history["last_update"] = datetime.now().isoformat()
|
||||||
|
self.save_download_history()
|
||||||
|
|
||||||
|
print(f"\nDownload session complete. Downloaded {total_downloads} new patches.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scraper = RomhackRaceScraper()
|
||||||
|
|
||||||
|
# Check if we have existing downloads
|
||||||
|
if os.path.exists("download_history.json"):
|
||||||
|
print("Found existing download history")
|
||||||
|
print(f"Last update: {scraper.download_history['last_update']}")
|
||||||
|
print(f"Previously downloaded patches: {len(scraper.download_history['downloaded_patches'])}")
|
||||||
|
print("Checking for new patches...\n")
|
||||||
|
else:
|
||||||
|
print("No download history found. Will download all patches.\n")
|
||||||
|
|
||||||
|
scraper.scrape_all_seasons()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
91
patch_roms.sh
Executable file
91
patch_roms.sh
Executable file
@ -0,0 +1,91 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Function to show usage
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage: $0 [-d|--dry-run] <path_to_original_rom>"
|
||||||
|
echo "Options:"
|
||||||
|
echo " -d, --dry-run Show what would be patched without actually patching"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
DRY_RUN=0
|
||||||
|
ORIGINAL_ROM=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-d|--dry-run)
|
||||||
|
DRY_RUN=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ORIGINAL_ROM="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if original ROM was provided
|
||||||
|
if [ -z "$ORIGINAL_ROM" ]; then
|
||||||
|
show_usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
PATCHES_DIR="./patches"
|
||||||
|
OUTPUT_DIR="./patched"
|
||||||
|
|
||||||
|
# Check if original ROM exists
|
||||||
|
if [ ! -f "$ORIGINAL_ROM" ]; then
|
||||||
|
echo "Error: Original ROM file not found: $ORIGINAL_ROM"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if flips is installed (skip check in dry run)
|
||||||
|
if [ $DRY_RUN -eq 0 ]; then
|
||||||
|
if ! command -v flips &> /dev/null; then
|
||||||
|
echo "Error: flips is not installed or not in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist (skip in dry run)
|
||||||
|
if [ $DRY_RUN -eq 0 ]; then
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$([ $DRY_RUN -eq 1 ] && echo '[DRY RUN] ')Processing patches..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Find all .bps files and extract week numbers
|
||||||
|
while IFS= read -r patch_file; do
|
||||||
|
# Extract week number from filename
|
||||||
|
if [[ $patch_file =~ Week([0-9]+)\.bps$ ]]; then
|
||||||
|
week_num="${BASH_REMATCH[1]}"
|
||||||
|
# Create zero-padded week number
|
||||||
|
padded_week=$(printf "Week%03d.smc" "$week_num")
|
||||||
|
output_file="$OUTPUT_DIR/$padded_week"
|
||||||
|
|
||||||
|
if [ $DRY_RUN -eq 1 ]; then
|
||||||
|
echo "[DRY RUN] Would patch: $patch_file"
|
||||||
|
echo "[DRY RUN] Would create: $output_file"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
echo "Patching: $patch_file -> $padded_week"
|
||||||
|
|
||||||
|
# Apply patch using flips
|
||||||
|
flips -a "$patch_file" "$ORIGINAL_ROM" "$output_file"
|
||||||
|
|
||||||
|
# Check if patching was successful
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Successfully created $padded_week"
|
||||||
|
else
|
||||||
|
echo "Error patching $patch_file"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: Could not extract week number from filename: $patch_file"
|
||||||
|
fi
|
||||||
|
done < <(find "$PATCHES_DIR" -type f -name "Week*.bps" | sort -V)
|
||||||
|
|
||||||
|
total_files=$(find "$PATCHES_DIR" -type f -name "Week*.bps" | wc -l)
|
||||||
|
echo "$([ $DRY_RUN -eq 1 ] && echo '[DRY RUN] ')Would create $total_files patched ROMs in $OUTPUT_DIR"
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
beautifulsoup4
|
Loading…
x
Reference in New Issue
Block a user