mirror of
https://github.com/lesserkuma/GBA_MultiMenu.git
synced 2026-08-23 09:04:28 -05:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a7b04e1ee | ||
|
|
3e0a6e89cb | ||
|
|
89b22c8205 | ||
|
|
39ab8936ec | ||
|
|
a2fa155e8e | ||
|
|
f756a705ae | ||
|
|
d51437552e | ||
|
|
12d953d8fd | ||
|
|
1bf4927d6b | ||
|
|
a3c4a44893 | ||
|
|
cbe17ef7c5 | ||
|
|
8dc1d10a95 | ||
|
|
bcb9ae5d65 |
179
.github/workflows/build.yaml
vendored
Normal file
179
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
name: Build release ZIP
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version for ZIP filename, e.g. v1.2.3'
|
||||||
|
required: false
|
||||||
|
default: manual
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-gba:
|
||||||
|
name: Build GBA ROM
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Build lk_multimenu.gba with devkitPro
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "${{ github.workspace }}:/workspace" \
|
||||||
|
-w /workspace \
|
||||||
|
devkitpro/devkitarm:latest \
|
||||||
|
bash -lc 'make clean && make'
|
||||||
|
|
||||||
|
- name: Verify GBA output
|
||||||
|
run: test -f rom_builder/lk_multimenu.gba
|
||||||
|
|
||||||
|
- name: Upload GBA artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gba-rom
|
||||||
|
path: rom_builder/lk_multimenu.gba
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build-rom-builder:
|
||||||
|
name: Build rom_builder.exe
|
||||||
|
runs-on: windows-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install PyInstaller
|
||||||
|
run: python -m pip install --upgrade pip pyinstaller
|
||||||
|
|
||||||
|
- name: Build rom_builder.exe
|
||||||
|
run: pyinstaller --noconfirm --clean --onefile --console --name rom_builder rom_builder/rom_builder.py
|
||||||
|
|
||||||
|
- name: Verify EXE output
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if (-not (Test-Path "dist/rom_builder.exe")) {
|
||||||
|
throw "dist/rom_builder.exe was not created"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload EXE artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rom-builder-exe
|
||||||
|
path: dist/rom_builder.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
package:
|
||||||
|
name: Package ZIP
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs:
|
||||||
|
- build-gba
|
||||||
|
- build-rom-builder
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Download GBA artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gba-rom
|
||||||
|
path: artifacts/gba
|
||||||
|
|
||||||
|
- name: Download EXE artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rom-builder-exe
|
||||||
|
path: artifacts/exe
|
||||||
|
|
||||||
|
- name: Determine ZIP name
|
||||||
|
id: package_name
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
RELEASE_VERSION: ${{ github.event.release.tag_name }}
|
||||||
|
MANUAL_VERSION: ${{ inputs.version }}
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
if ($env:EVENT_NAME -eq "release") {
|
||||||
|
$version = $env:RELEASE_VERSION
|
||||||
|
} else {
|
||||||
|
$version = $env:MANUAL_VERSION
|
||||||
|
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||||
|
$version = $env:REF_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeVersion = $version -replace '[^A-Za-z0-9._-]', '_'
|
||||||
|
$zipName = "lk_multimenu_rom_builder_$safeVersion.zip"
|
||||||
|
$zipPath = "release/$zipName"
|
||||||
|
|
||||||
|
"version=$safeVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
|
"zip_name=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
|
"zip_path=$zipPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
|
|
||||||
|
Write-Host "ZIP file: $zipName"
|
||||||
|
|
||||||
|
- name: Create release ZIP
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$packageDir = "package"
|
||||||
|
$releaseDir = "release"
|
||||||
|
$zipPath = "${{ steps.package_name.outputs.zip_path }}"
|
||||||
|
|
||||||
|
Remove-Item $packageDir, $releaseDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
New-Item -ItemType Directory -Force -Path $packageDir, "$packageDir/roms", $releaseDir | Out-Null
|
||||||
|
|
||||||
|
Copy-Item "rom_builder/bg.png" "$packageDir/bg.png" -Force
|
||||||
|
Copy-Item "rom_builder/rom_builder.py" "$packageDir/rom_builder.py" -Force
|
||||||
|
Copy-Item "artifacts/gba/lk_multimenu.gba" "$packageDir/lk_multimenu.gba" -Force
|
||||||
|
Copy-Item "artifacts/exe/rom_builder.exe" "$packageDir/rom_builder.exe" -Force
|
||||||
|
|
||||||
|
if (Test-Path "rom_builder/roms") {
|
||||||
|
Copy-Item "rom_builder/roms/*" "$packageDir/roms/" -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Get-ChildItem "$packageDir/roms" -Force -ErrorAction SilentlyContinue)) {
|
||||||
|
New-Item -ItemType File -Path "$packageDir/roms/- ROM files go here -" -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Compress-Archive -Path "$packageDir/*" -DestinationPath $zipPath -Force
|
||||||
|
|
||||||
|
Write-Host "ZIP contents:"
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
[System.IO.Compression.ZipFile]::OpenRead($zipPath).Entries.FullName | Sort-Object | ForEach-Object { Write-Host $_ }
|
||||||
|
|
||||||
|
- name: Upload workflow artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.package_name.outputs.zip_name }}
|
||||||
|
path: ${{ steps.package_name.outputs.zip_path }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Attach ZIP to GitHub Release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: gh release upload '${{ github.event.release.tag_name }}' '${{ steps.package_name.outputs.zip_path }}' --clobber
|
||||||
2
Makefile
2
Makefile
@@ -183,7 +183,9 @@ $(shell touch $(CURDIR)/../$(SOURCES)/version.h)
|
|||||||
%.gba: %.elf
|
%.gba: %.elf
|
||||||
@$(OBJCOPY) -O binary $< $@
|
@$(OBJCOPY) -O binary $< $@
|
||||||
@gbafix $@ "-tLK MULTIMENU" "-cAGBJ" "-mLK" "-r0"
|
@gbafix $@ "-tLK MULTIMENU" "-cAGBJ" "-mLK" "-r0"
|
||||||
|
@echo Copying to ROM builder folder
|
||||||
@cp $@ "$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))/rom_builder/lk_multimenu.gba"
|
@cp $@ "$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))/rom_builder/lk_multimenu.gba"
|
||||||
|
@echo Done!
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------------
|
#---------------------------------------------------------------------------------------
|
||||||
endif
|
endif
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -15,15 +15,21 @@ The following section must be edited in order to specify the cartridge type to u
|
|||||||
```json
|
```json
|
||||||
"cartridge": {
|
"cartridge": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
"battery_present": false
|
"battery_present": false,
|
||||||
|
"min_rom_size": 4194304
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
Set `type` to `1` or `2`:
|
Set `type` to one of these:
|
||||||
- `1` = MSP55LV100S (e.g. The Legend of Zelda Collection - Classic Edition 7-in-1)
|
- `1` = MSP55LV100S (e.g. The Legend of Zelda Collection - Classic Edition 7-in-1, 64 MiB)
|
||||||
- `2` = 6600M0U0BE (e.g. 369IN1 2048M)
|
- `2` = 6600M0U0BE (e.g. 369IN1 2048M, 256 MiB)
|
||||||
|
- `3` = MSP54LV100 (e.g. The Legend of Zelda Collection - Classic Edition 7-in-1, 128 MiB)
|
||||||
|
- `4` = F0095H0 (e.g. 53 in one 4G, 512 MiB)
|
||||||
|
- `5` = GL04GR00FHCR2 (e.g. 369IN1 2048M, 256 MiB, 2026)
|
||||||
|
|
||||||
Set `battery_present` to `true` or `false`. This will enable enhanced save data handling which will only be functional with a working battery.
|
Set `battery_present` to `true` or `false`. This will enable enhanced save data handling which will only be functional with a working battery.
|
||||||
|
|
||||||
|
Set `min_rom_size` to whatever your cartridge supports as the smallest possible ROM size. Many newer cartridges only support ROMs no smaller than 4 MiB (`4194304`) while some older cartridges can go as low as 512 KiB (`524288`).
|
||||||
|
|
||||||
In the `games` section, you can edit the game-related stuff:
|
In the `games` section, you can edit the game-related stuff:
|
||||||
```json
|
```json
|
||||||
"games": [
|
"games": [
|
||||||
@@ -47,6 +53,7 @@ In the `games` section, you can edit the game-related stuff:
|
|||||||
- `6` = Pokémon Black & White condensed battle font
|
- `6` = Pokémon Black & White condensed battle font
|
||||||
- `save_slot` defines which save slot your game uses. Set it to `null` for no saving or a number starting from `1`. Multiple games can share a save slot.
|
- `save_slot` defines which save slot your game uses. Set it to `null` for no saving or a number starting from `1`. Multiple games can share a save slot.
|
||||||
- `map_256m`, if set to `true`, can serve as a workaround for a glitch with the cartridge mapper that causes games to freeze with screeching noises upon launch.
|
- `map_256m`, if set to `true`, can serve as a workaround for a glitch with the cartridge mapper that causes games to freeze with screeching noises upon launch.
|
||||||
|
- `keys` will let you specify a list of keys that must be held down at startup for this ROM to appear in the menu, e.g. `[ "L", "R", "DOWN" ]`.
|
||||||
|
|
||||||
### ROM Builder Command Line Arguments
|
### ROM Builder Command Line Arguments
|
||||||
|
|
||||||
@@ -57,6 +64,7 @@ No command line arguments are required for creating a compilation, however there
|
|||||||
--no-wait don't wait for user input when finished
|
--no-wait don't wait for user input when finished
|
||||||
--no-log don't write a log file
|
--no-log don't write a log file
|
||||||
--config config.json sets the config file to use
|
--config config.json sets the config file to use
|
||||||
|
--bg bg.png sets the background image to use
|
||||||
--output output.gba sets the file name of the compilation ROM
|
--output output.gba sets the file name of the compilation ROM
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -67,19 +75,22 @@ No command line arguments are required for creating a compilation, however there
|
|||||||
- up to 64 KiB of save data per ROM
|
- up to 64 KiB of save data per ROM
|
||||||
|
|
||||||
### Save Data
|
### Save Data
|
||||||
If the cartridge has a battery installed, the ROMs must be SRAM-patched with [GBATA](https://www.romhacking.net/utilities/601/) for saving to work.
|
If the cartridge has a battery installed, the ROMs must be SRAM-patched for saving to work. If the cartridge has no battery installed, the ROMs must be patched for the Batteryless SRAM method. If your cartridge has the GL04GR00FHCR2 flash chip or similar, the ROMs must be patched for slower cartridge access timing (waitstate). The [GBA Save Type Patcher](https://lesserkuma.github.io/GBA_Save_Type_Patcher/) offers these options.
|
||||||
|
|
||||||
If the cartridge has no battery installed, the ROMs must be patched for batteryless SRAM saving with maniac's [Automatic batteryless saving patcher](https://github.com/metroid-maniac/gba-auto-batteryless-patcher/).
|
On battery-equipped cartridges, when starting a game from the menu, the previously played game's save data will be read from SRAM and stored to permanent flash memory. To skip this, you can hold the SELECT button while starting the game.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
Tested repro cartridges:
|
Tested repro cartridges:
|
||||||
- 100BS6600_48BALL_V4 with 6600M0U0BE
|
|
||||||
- 100SOP with MSP55LV100S
|
- 100SOP with MSP55LV100S
|
||||||
|
- 100BS6600_48BALL_V4 with 6600M0U0BE
|
||||||
|
- SUN100S_MSP54_XXX_BGA48 with MSP54LV100
|
||||||
|
- F0095_4G_V1 with F0095H0
|
||||||
|
- ACAM_GL04G10_2G_204 with GL04GR00FHCR2
|
||||||
|
|
||||||
The generated compilation ROM can be written and read using a [GBxCart RW v1.4+](https://www.gbxcart.com/) device by insideGadgets and the [FlashGBX](https://github.com/lesserkuma/FlashGBX) software.
|
The generated compilation ROM can be written and read using a compatible cart flasher device and the [FlashGBX](https://github.com/lesserkuma/FlashGBX) software.
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
Thanks to FraX, Ausar, liuyunx, BennVenn
|
Thanks to FraX, Ausar, liuyunx, BennVenn, Jenetrix, Matt
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
|||||||
BIN
graphics/bg.png
BIN
graphics/bg.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 756 B |
BIN
rom_builder/bg.png
Normal file
BIN
rom_builder/bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 756 B |
@@ -5,7 +5,7 @@
|
|||||||
import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime
|
import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
app_version = "0.3"
|
app_version = "1.2"
|
||||||
default_file = "LK_MULTIMENU_<CODE>.gba"
|
default_file = "LK_MULTIMENU_<CODE>.gba"
|
||||||
|
|
||||||
################################
|
################################
|
||||||
@@ -14,7 +14,6 @@ def UpdateSectorMap(start, length, c):
|
|||||||
sector_map[start + 1:start + length] = c * (length - 1)
|
sector_map[start + 1:start + length] = c * (length - 1)
|
||||||
sector_map[start] = c.upper()
|
sector_map[start] = c.upper()
|
||||||
|
|
||||||
|
|
||||||
def formatFileSize(size):
|
def formatFileSize(size):
|
||||||
if size == 1:
|
if size == 1:
|
||||||
return "{:d} Byte".format(size)
|
return "{:d} Byte".format(size)
|
||||||
@@ -51,6 +50,24 @@ cartridge_types = [
|
|||||||
"sector_size":0x40000,
|
"sector_size":0x40000,
|
||||||
"block_size":0x80000,
|
"block_size":0x80000,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"MSP54LV100",
|
||||||
|
"flash_size":0x8000000,
|
||||||
|
"sector_size":0x20000,
|
||||||
|
"block_size":0x80000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"F0095H0",
|
||||||
|
"flash_size":0x20000000,
|
||||||
|
"sector_size":0x40000,
|
||||||
|
"block_size":0x80000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"GL04GR00FHCR2",
|
||||||
|
"flash_size":0x10000000,
|
||||||
|
"sector_size":0x20000,
|
||||||
|
"block_size":0x80000,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
log = ""
|
log = ""
|
||||||
@@ -62,6 +79,7 @@ parser.add_argument("--split", help="splits output files into 32 MiB parts", act
|
|||||||
parser.add_argument("--no-wait", help="don’t wait for user input when finished", action="store_true", default=False)
|
parser.add_argument("--no-wait", help="don’t wait for user input when finished", action="store_true", default=False)
|
||||||
parser.add_argument("--no-log", help="don’t write a log file", action="store_true", default=False)
|
parser.add_argument("--no-log", help="don’t write a log file", action="store_true", default=False)
|
||||||
parser.add_argument("--config", type=str, default="config.json", help="sets the config file to use")
|
parser.add_argument("--config", type=str, default="config.json", help="sets the config file to use")
|
||||||
|
parser.add_argument("--bg", type=str, help="sets the background image to use")
|
||||||
parser.add_argument("--output", type=str, default=default_file, help="sets the file name of the compilation ROM")
|
parser.add_argument("--output", type=str, default=default_file, help="sets the file name of the compilation ROM")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
output_file = args.output
|
output_file = args.output
|
||||||
@@ -83,19 +101,27 @@ if not os.path.exists(args.config):
|
|||||||
games = []
|
games = []
|
||||||
cartridge_type = 1
|
cartridge_type = 1
|
||||||
battery_present = False
|
battery_present = False
|
||||||
|
min_rom_size = 0x400000
|
||||||
for file in files:
|
for file in files:
|
||||||
games.append({
|
d = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"file": os.path.split(file)[1],
|
"file": os.path.split(file)[1],
|
||||||
"title": os.path.splitext(os.path.split(file)[1])[0],
|
"title": os.path.splitext(os.path.split(file)[1])[0],
|
||||||
"title_font": 1,
|
"title_font": 1,
|
||||||
"save_slot": save_slot,
|
"save_slot": save_slot,
|
||||||
})
|
}
|
||||||
|
with open(file, "rb") as f:
|
||||||
|
f.seek(0xAC)
|
||||||
|
code = f.read(0x4)
|
||||||
|
if code[:3] in (b"BPG", b"BPR"):
|
||||||
|
d["map_256m"] = True
|
||||||
|
games.append(d)
|
||||||
save_slot += 1
|
save_slot += 1
|
||||||
obj = {
|
obj = {
|
||||||
"cartridge": {
|
"cartridge": {
|
||||||
"type": cartridge_type + 1,
|
"type": cartridge_type + 1,
|
||||||
"battery_present": battery_present,
|
"battery_present": battery_present,
|
||||||
|
"min_rom_size": min_rom_size,
|
||||||
},
|
},
|
||||||
"games": games,
|
"games": games,
|
||||||
}
|
}
|
||||||
@@ -118,6 +144,10 @@ else:
|
|||||||
games = j["games"]
|
games = j["games"]
|
||||||
cartridge_type = j["cartridge"]["type"] - 1
|
cartridge_type = j["cartridge"]["type"] - 1
|
||||||
battery_present = j["cartridge"]["battery_present"]
|
battery_present = j["cartridge"]["battery_present"]
|
||||||
|
if "min_rom_size" in j["cartridge"]:
|
||||||
|
min_rom_size = j["cartridge"]["min_rom_size"]
|
||||||
|
else:
|
||||||
|
min_rom_size = 0x400000
|
||||||
|
|
||||||
# Prepare compilation
|
# Prepare compilation
|
||||||
flash_size = cartridge_types[cartridge_type]["flash_size"]
|
flash_size = cartridge_types[cartridge_type]["flash_size"]
|
||||||
@@ -127,6 +157,7 @@ block_size = cartridge_types[cartridge_type]["block_size"]
|
|||||||
block_count = flash_size // block_size
|
block_count = flash_size // block_size
|
||||||
sectors_per_block = 0x80000 // sector_size
|
sectors_per_block = 0x80000 // sector_size
|
||||||
compilation = bytearray()
|
compilation = bytearray()
|
||||||
|
roms_keys = [0]
|
||||||
for i in range(flash_size // 0x2000000):
|
for i in range(flash_size // 0x2000000):
|
||||||
chunk = bytearray([0xFF] * 0x2000000)
|
chunk = bytearray([0xFF] * 0x2000000)
|
||||||
compilation += chunk
|
compilation += chunk
|
||||||
@@ -134,7 +165,36 @@ sector_map = list("." * sector_count)
|
|||||||
|
|
||||||
# Read menu ROM
|
# Read menu ROM
|
||||||
with open("lk_multimenu.gba", "rb") as f:
|
with open("lk_multimenu.gba", "rb") as f:
|
||||||
menu_rom = f.read()
|
menu_rom = bytearray(f.read())
|
||||||
|
menu_rom += bytearray([0xFF] * ((len(menu_rom) + 0x10 - (len(menu_rom) % 0x10)) - len(menu_rom)))
|
||||||
|
menu_rom += bytearray([0xFF] * 0x20)
|
||||||
|
build_timestamp_offset = len(menu_rom) - 0x20
|
||||||
|
build_timestamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat().encode("ASCII")
|
||||||
|
menu_rom[build_timestamp_offset:build_timestamp_offset+len(build_timestamp)] = build_timestamp
|
||||||
|
|
||||||
|
# Change background image
|
||||||
|
if args.bg or os.path.exists("bg.png"):
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
if args.bg:
|
||||||
|
img = Image.open(args.bg)
|
||||||
|
else:
|
||||||
|
img = Image.open("bg.png")
|
||||||
|
img = img.convert('P')
|
||||||
|
palette = img.getpalette()
|
||||||
|
palette_rgb555 = [((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3) for r, g, b in zip(palette[::3], palette[1::3], palette[2::3])]
|
||||||
|
raw_bitmap = bytearray(list(img.tobytes()))
|
||||||
|
raw_palette = bytearray(0x200)
|
||||||
|
pos = 0
|
||||||
|
for color in palette_rgb555:
|
||||||
|
raw_palette[pos:pos+2] = struct.pack("<H", color)
|
||||||
|
pos += 2
|
||||||
|
menu_rom_bg_offset = menu_rom.find(b"RTFN\xFF\xFE") - 0x9800
|
||||||
|
menu_rom[menu_rom_bg_offset:menu_rom_bg_offset+0x9600] = raw_bitmap
|
||||||
|
menu_rom[menu_rom_bg_offset+0x9600:menu_rom_bg_offset+0x9800] = raw_palette
|
||||||
|
except ImportError:
|
||||||
|
print("Error: Couldn’t update background image. Pillow library is not installed.")
|
||||||
|
|
||||||
menu_rom_size = menu_rom.find(b"dkARM\0\0\0") + 8
|
menu_rom_size = menu_rom.find(b"dkARM\0\0\0") + 8
|
||||||
compilation[0:len(menu_rom)] = menu_rom
|
compilation[0:len(menu_rom)] = menu_rom
|
||||||
UpdateSectorMap(start=0, length=math.ceil(len(menu_rom) / sector_size), c="m")
|
UpdateSectorMap(start=0, length=math.ceil(len(menu_rom) / sector_size), c="m")
|
||||||
@@ -166,6 +226,13 @@ for game in games:
|
|||||||
x = 0x80000
|
x = 0x80000
|
||||||
while (x < size): x *= 2
|
while (x < size): x *= 2
|
||||||
size = x
|
size = x
|
||||||
|
if size < 0x400000:
|
||||||
|
with open(f"roms/{game['file']}", "rb") as f:
|
||||||
|
buffer = f.read()
|
||||||
|
if b"Batteryless mod by Lesserkuma" in buffer:
|
||||||
|
size = max(0x400000, min_rom_size)
|
||||||
|
else:
|
||||||
|
size = max(size, min_rom_size)
|
||||||
game["index"] = index
|
game["index"] = index
|
||||||
game["size"] = size
|
game["size"] = size
|
||||||
if "title_font" in game:
|
if "title_font" in game:
|
||||||
@@ -173,7 +240,37 @@ for game in games:
|
|||||||
else:
|
else:
|
||||||
game["title_font"] = 0
|
game["title_font"] = 0
|
||||||
game["sector_count"] = int(size / sector_size)
|
game["sector_count"] = int(size / sector_size)
|
||||||
|
|
||||||
|
# Hidden ROMs
|
||||||
|
keys = 0
|
||||||
|
if "keys" in game:
|
||||||
|
for key in game["keys"]:
|
||||||
|
if key.upper() == "A":
|
||||||
|
keys |= (1 << 0)
|
||||||
|
elif key.upper() == "B":
|
||||||
|
keys |= (1 << 1)
|
||||||
|
elif key.upper() == "SELECT":
|
||||||
|
keys |= (1 << 2)
|
||||||
|
elif key.upper() == "START":
|
||||||
|
keys |= (1 << 3)
|
||||||
|
elif key.upper() == "RIGHT":
|
||||||
|
keys |= (1 << 4)
|
||||||
|
elif key.upper() == "LEFT":
|
||||||
|
keys |= (1 << 5)
|
||||||
|
elif key.upper() == "UP":
|
||||||
|
keys |= (1 << 6)
|
||||||
|
elif key.upper() == "DOWN":
|
||||||
|
keys |= (1 << 7)
|
||||||
|
elif key.upper() == "R":
|
||||||
|
keys |= (1 << 8)
|
||||||
|
elif key.upper() == "L":
|
||||||
|
keys |= (1 << 9)
|
||||||
|
game["keys"] = keys
|
||||||
|
|
||||||
|
if keys > 0:
|
||||||
|
roms_keys.append(keys)
|
||||||
|
roms_keys = list(set(roms_keys))
|
||||||
|
|
||||||
if battery_present and game["save_slot"] is not None:
|
if battery_present and game["save_slot"] is not None:
|
||||||
game["save_type"] = 2
|
game["save_type"] = 2
|
||||||
game["save_slot"] -= 1
|
game["save_slot"] -= 1
|
||||||
@@ -215,7 +312,6 @@ for game in games:
|
|||||||
|
|
||||||
# Read ROM data
|
# Read ROM data
|
||||||
games.sort(key=lambda game: game["size"], reverse=True)
|
games.sort(key=lambda game: game["size"], reverse=True)
|
||||||
c = 0
|
|
||||||
for game in games:
|
for game in games:
|
||||||
found = False
|
found = False
|
||||||
for i in range(save_end_offset, len(sector_map)):
|
for i in range(save_end_offset, len(sector_map)):
|
||||||
@@ -259,40 +355,51 @@ logp("{:.2f}% ({:d} of {:d} sectors) used\n".format(sectors_used / sector_count
|
|||||||
logp(f"Added {len(games)} ROM(s) to the compilation\n")
|
logp(f"Added {len(games)} ROM(s) to the compilation\n")
|
||||||
|
|
||||||
if battery_present:
|
if battery_present:
|
||||||
logp (" | Offset | Map Size | Save Slot | Title")
|
logp (" | Offset | Map Size | Save Slot | Title")
|
||||||
toc_sep = "----+-----------+-----------+----------------+---------------------------------"
|
toc_sep = "----+------------+-----------+----------------+--------------------------------"
|
||||||
else:
|
else:
|
||||||
logp (" | Offset | Map Size | Title")
|
logp (" | Offset | Map Size | Title")
|
||||||
toc_sep = "----+-----------+-----------+--------------------------------------------------"
|
toc_sep = "----+------------+-----------+-------------------------------------------------"
|
||||||
|
|
||||||
item_list = bytearray()
|
item_list = bytearray()
|
||||||
for game in games:
|
|
||||||
title = game["title"]
|
|
||||||
if len(title) > 0x30: title = title[:0x2F] + "…"
|
|
||||||
|
|
||||||
table_line = \
|
for key in roms_keys:
|
||||||
f"{game['index'] + 1:3d} | " \
|
c = 0
|
||||||
f"0x{game['block_offset'] * block_size:07X} | "\
|
for game in games:
|
||||||
f"0x{game['block_count'] * block_size:07X} | "
|
if game["keys"] != key: continue
|
||||||
if battery_present:
|
|
||||||
if game['save_type'] > 0:
|
title = game["title"]
|
||||||
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
|
if len(title) > 0x30: title = title[:0x2F] + "…"
|
||||||
else:
|
|
||||||
table_line += " | "
|
table_line = \
|
||||||
table_line += f"{title}"
|
f"{game['index'] + 1:3d} | " + \
|
||||||
if c % 8 == 0: logp(toc_sep)
|
f"0x{game['block_offset'] * block_size:X} | ".rjust(13, " ") + \
|
||||||
logp(table_line)
|
f"0x{game['block_count'] * block_size:X} | ".rjust(12, " ")
|
||||||
c += 1
|
if battery_present:
|
||||||
|
if game['save_type'] > 0:
|
||||||
title = title.ljust(0x30, "\0")
|
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
|
||||||
item_list += bytearray(struct.pack("B", game["title_font"]))
|
else:
|
||||||
item_list += bytearray(struct.pack("B", len(game["title"])))
|
table_line += " | "
|
||||||
item_list += bytearray(struct.pack("<H", game["block_offset"]))
|
table_line += f"{title}"
|
||||||
item_list += bytearray(struct.pack("<H", game["block_count"]))
|
if c % 8 == 0:
|
||||||
item_list += bytearray(struct.pack("B", game["save_type"]))
|
if game['keys'] != 0:
|
||||||
item_list += bytearray(struct.pack("B", game["save_slot"]))
|
temp = toc_sep[:-9] + "[Hidden]-"
|
||||||
item_list += bytearray([0] * 8)
|
logp(temp)
|
||||||
item_list += bytearray(title.encode("UTF-16LE"))
|
else:
|
||||||
|
logp(toc_sep)
|
||||||
|
logp(table_line)
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
title = title.ljust(0x30, "\0")
|
||||||
|
item_list += bytearray(struct.pack("B", game["title_font"]))
|
||||||
|
item_list += bytearray(struct.pack("B", len(game["title"])))
|
||||||
|
item_list += bytearray(struct.pack("<H", game["block_offset"]))
|
||||||
|
item_list += bytearray(struct.pack("<H", game["block_count"]))
|
||||||
|
item_list += bytearray(struct.pack("B", game["save_type"]))
|
||||||
|
item_list += bytearray(struct.pack("B", game["save_slot"]))
|
||||||
|
item_list += bytearray(struct.pack("<H", game["keys"]))
|
||||||
|
item_list += bytearray([0] * 6)
|
||||||
|
item_list += bytearray(title.encode("UTF-16LE"))
|
||||||
|
|
||||||
compilation[item_list_offset * sector_size:item_list_offset * sector_size + len(item_list)] = item_list
|
compilation[item_list_offset * sector_size:item_list_offset * sector_size + len(item_list)] = item_list
|
||||||
rom_code = "L{:s}".format(hashlib.sha1(status + item_list).hexdigest()[:3]).upper()
|
rom_code = "L{:s}".format(hashlib.sha1(status + item_list).hexdigest()[:3]).upper()
|
||||||
@@ -306,21 +413,25 @@ for i in range(0xA0, 0xBD):
|
|||||||
checksum = (checksum - 0x19) & 0xFF
|
checksum = (checksum - 0x19) & 0xFF
|
||||||
compilation[0xBD] = checksum
|
compilation[0xBD] = checksum
|
||||||
logp("")
|
logp("")
|
||||||
logp("Menu ROM: 0x{:07X}–0x{:07X}".format(0, len(menu_rom)))
|
logp("Menu ROM: 0x{:08X}–0x{:08X}".format(0, len(menu_rom)))
|
||||||
logp("Game List: 0x{:07X}–0x{:07X}".format(item_list_offset * sector_size, item_list_offset * sector_size + len(item_list)))
|
logp("Game List: 0x{:08X}–0x{:08X}".format(item_list_offset * sector_size, item_list_offset * sector_size + len(item_list)))
|
||||||
logp("Status Area: 0x{:07X}–0x{:07X}".format(status_offset * sector_size, status_offset * sector_size + 0x1000))
|
logp("Status Area: 0x{:08X}–0x{:08X}".format(status_offset * sector_size, status_offset * sector_size + 0x1000))
|
||||||
logp("")
|
logp("")
|
||||||
logp("Cartridge Type: {:d} ({:s} {:s})".format(cartridge_type, cartridge_types[cartridge_type]["name"], "with battery" if battery_present else "without battery"))
|
logp("Cartridge Type: {:d} ({:s}) {:s}".format(cartridge_type + 1, cartridge_types[cartridge_type]["name"], "with battery" if battery_present else "without battery"))
|
||||||
logp("Output ROM Size: {:.2f} MiB".format(rom_size / 1024 / 1024))
|
logp("Output ROM Size: {:.2f} MiB".format(rom_size / 1024 / 1024))
|
||||||
logp("Output ROM Code: {:s}".format(rom_code))
|
logp("Output ROM Code: {:s}".format(rom_code))
|
||||||
|
if cartridge_type + 1 == 5:
|
||||||
|
logp("")
|
||||||
|
logp("Note: The GL04GR00FHCR2 flash chip may be too slow to work with most commercial games. Games may need to be patched for slower access timing via the Waitstate Control register.")
|
||||||
output_file = output_file.replace("<CODE>", rom_code)
|
output_file = output_file.replace("<CODE>", rom_code)
|
||||||
|
|
||||||
if args.split:
|
if args.split:
|
||||||
for i in range(0, math.ceil(flash_size / 0x2000000)):
|
for i in range(0, math.ceil(flash_size / 0x2000000)):
|
||||||
pos = i * 0x2000000
|
pos = i * 0x2000000
|
||||||
size = 0x2000000
|
size = 0x2000000
|
||||||
if pos > len(compilation[:rom_size]): break
|
if pos > len(compilation[:rom_size]): break
|
||||||
if pos + size > rom_size: size = rom_size - pos
|
if pos + size > rom_size: size = rom_size - pos
|
||||||
output_file_part = "{:s}_part{:d}{:s}".format(os.path.splitext(output_file)[0], i + 1, os.path.splitext(output_file)[1])
|
output_file_part = "{:s}_part{:d}{:s}".format(os.path.splitext(output_file)[0], i, os.path.splitext(output_file)[1])
|
||||||
with open(output_file_part, "wb") as f: f.write(compilation[pos:pos+size])
|
with open(output_file_part, "wb") as f: f.write(compilation[pos:pos+size])
|
||||||
else:
|
else:
|
||||||
with open(output_file, "wb") as f: f.write(compilation[:rom_size])
|
with open(output_file, "wb") as f: f.write(compilation[:rom_size])
|
||||||
|
|||||||
156
source/flash.c
156
source/flash.c
@@ -12,7 +12,9 @@ Author: Lesserkuma (github.com/lesserkuma)
|
|||||||
|
|
||||||
u8 flash_type;
|
u8 flash_type;
|
||||||
u8 *itemlist;
|
u8 *itemlist;
|
||||||
|
u16 itemlist_offset;
|
||||||
u32 flash_sector_size;
|
u32 flash_sector_size;
|
||||||
|
u16 flash_buffer_size;
|
||||||
u32 flash_itemlist_sector_offset;
|
u32 flash_itemlist_sector_offset;
|
||||||
u32 flash_status_sector_offset;
|
u32 flash_status_sector_offset;
|
||||||
u32 flash_save_sector_offset;
|
u32 flash_save_sector_offset;
|
||||||
@@ -46,6 +48,7 @@ IWRAM_CODE void FlashDetectType(void)
|
|||||||
REG_IE = ie;
|
REG_IE = ie;
|
||||||
flash_type = 1;
|
flash_type = 1;
|
||||||
flash_sector_size = 0x40000;
|
flash_sector_size = 0x40000;
|
||||||
|
flash_buffer_size = 0x400;
|
||||||
FlashCalcOffsets();
|
FlashCalcOffsets();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -62,6 +65,35 @@ IWRAM_CODE void FlashDetectType(void)
|
|||||||
REG_IE = ie;
|
REG_IE = ie;
|
||||||
flash_type = 2;
|
flash_type = 2;
|
||||||
flash_sector_size = 0x20000;
|
flash_sector_size = 0x20000;
|
||||||
|
flash_buffer_size = 0x20;
|
||||||
|
FlashCalcOffsets();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1G cart with MSP54LV100S (Zelda Classic Collection 7-in-1)
|
||||||
|
// 2G cart with GL04GR00FHCR2 (369-in-1)
|
||||||
|
_FLASH_WRITE(0, 0xF0);
|
||||||
|
_FLASH_WRITE(0xAAA, 0xA9);
|
||||||
|
_FLASH_WRITE(0x555, 0x56);
|
||||||
|
_FLASH_WRITE(0xAAA, 0x90);
|
||||||
|
data = *(vu32 *)AGB_ROM;
|
||||||
|
_FLASH_WRITE(0, 0xF0);
|
||||||
|
if (data == 0x227D0002)
|
||||||
|
{
|
||||||
|
_FLASH_WRITE(0xAA, 0x98);
|
||||||
|
data = *(vu32 *)(AGB_ROM+0x54) & 0xFFFF;
|
||||||
|
if (((*(vu16 *)(AGB_ROM+0x20) & 0xFF) == 'R') &&
|
||||||
|
((*(vu16 *)(AGB_ROM+0x22) & 0xFF) == 'Q') &&
|
||||||
|
((*(vu16 *)(AGB_ROM+0x24) & 0xFF) == 'Z'))
|
||||||
|
{
|
||||||
|
data = (data & ~3U) | ((data & 1) << 1) | ((data & 2) >> 1);
|
||||||
|
}
|
||||||
|
_FLASH_WRITE(0, 0xF0);
|
||||||
|
|
||||||
|
REG_IE = ie;
|
||||||
|
flash_type = 3;
|
||||||
|
flash_sector_size = 0x20000;
|
||||||
|
flash_buffer_size = 1 << data;
|
||||||
FlashCalcOffsets();
|
FlashCalcOffsets();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,6 +151,24 @@ IWRAM_CODE void FlashEraseSector(u32 address)
|
|||||||
}
|
}
|
||||||
_FLASH_WRITE(address, 0xF0F0);
|
_FLASH_WRITE(address, 0xF0F0);
|
||||||
}
|
}
|
||||||
|
else if (_flash_type == 3)
|
||||||
|
{
|
||||||
|
_FLASH_WRITE(0xAAA, 0xA9);
|
||||||
|
_FLASH_WRITE(0x555, 0x56);
|
||||||
|
_FLASH_WRITE(0xAAA, 0x80);
|
||||||
|
_FLASH_WRITE(0xAAA, 0xA9);
|
||||||
|
_FLASH_WRITE(0x555, 0x56);
|
||||||
|
_FLASH_WRITE(address, 0x30);
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
__asm("nop");
|
||||||
|
if ((*((vu16 *)(AGB_ROM + address))) == 0xFFFF)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_FLASH_WRITE(address, 0xF0);
|
||||||
|
}
|
||||||
|
|
||||||
REG_IE = ie;
|
REG_IE = ie;
|
||||||
}
|
}
|
||||||
@@ -130,33 +180,35 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
|
|||||||
FlashDetectType();
|
FlashDetectType();
|
||||||
}
|
}
|
||||||
u8 _flash_type = flash_type;
|
u8 _flash_type = flash_type;
|
||||||
|
u16 _flash_buffer_size = flash_buffer_size;
|
||||||
|
u16 _flash_buffer_count = (_flash_buffer_size >> 1) - 1;
|
||||||
vu16 *p_rom = (vu16 *)(AGB_ROM + address);
|
vu16 *p_rom = (vu16 *)(AGB_ROM + address);
|
||||||
vu16 ie = REG_IE;
|
vu16 ie = REG_IE;
|
||||||
REG_IE = ie & 0xFFFE;
|
REG_IE = ie & 0xFFFE;
|
||||||
|
|
||||||
if (_flash_type == 1)
|
if (_flash_type == 1)
|
||||||
{
|
{
|
||||||
for (int j = 0; j < (int)(length / 0x400); j++)
|
for (int j = 0; j < (int)(length / _flash_buffer_size); j++)
|
||||||
{
|
{
|
||||||
_FLASH_WRITE(address + (j * 0x400), 0xEA);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0xEA);
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
__asm("nop");
|
__asm("nop");
|
||||||
if ((p_rom[(j * 0x200)] & 0x80) == 0x80)
|
if ((p_rom[(j * (_flash_buffer_size >> 1))] & 0x80) == 0x80)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_FLASH_WRITE(address + (j * 0x400), 0x1FF);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), _flash_buffer_count);
|
||||||
for (int i = 0; i < 0x400; i += 2)
|
for (int i = 0; i < _flash_buffer_size; i += 2)
|
||||||
{
|
{
|
||||||
_FLASH_WRITE(address + (j * 0x400) + i, data_buffer[(j * 0x400) + i + 1] << 8 | data_buffer[(j * 0x400) + i]);
|
_FLASH_WRITE(address + (j * _flash_buffer_size) + i, data_buffer[(j * _flash_buffer_size) + i + 1] << 8 | data_buffer[(j * _flash_buffer_size) + i]);
|
||||||
}
|
}
|
||||||
_FLASH_WRITE(address + (j * 0x400), 0xD0);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0xD0);
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
__asm("nop");
|
__asm("nop");
|
||||||
if ((p_rom[(j * 0x200)] & 0x80) == 0x80)
|
if ((p_rom[(j * (_flash_buffer_size >> 1))] & 0x80) == 0x80)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -166,24 +218,24 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
|
|||||||
}
|
}
|
||||||
else if (_flash_type == 2)
|
else if (_flash_type == 2)
|
||||||
{
|
{
|
||||||
for (int j = 0; j < (int)(length / 0x20); j++)
|
for (int j = 0; j < (int)(length / _flash_buffer_size); j++)
|
||||||
{
|
{
|
||||||
_FLASH_WRITE(0xAAA, 0xAAA9);
|
_FLASH_WRITE(0xAAA, 0xAAA9);
|
||||||
_FLASH_WRITE(0x555, 0x5556);
|
_FLASH_WRITE(0x555, 0x5556);
|
||||||
_FLASH_WRITE(address + (j * 0x20), 0x2526);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x2526);
|
||||||
_FLASH_WRITE(address + (j * 0x20), 0x0F0F);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), _flash_buffer_count | (_flash_buffer_count << 8));
|
||||||
u16 data = 0;
|
u16 data = 0;
|
||||||
for (int i = 0; i < 0x20; i += 2)
|
for (int i = 0; i < _flash_buffer_size; i += 2)
|
||||||
{
|
{
|
||||||
__asm("nop");
|
__asm("nop");
|
||||||
data = data_buffer[(j * 0x20) + i + 1] << 8 | data_buffer[(j * 0x20) + i];
|
data = data_buffer[(j * _flash_buffer_size) + i + 1] << 8 | data_buffer[(j * _flash_buffer_size) + i];
|
||||||
_FLASH_WRITE(address + (j * 0x20) + i, data);
|
_FLASH_WRITE(address + (j * _flash_buffer_size) + i, data);
|
||||||
}
|
}
|
||||||
_FLASH_WRITE(address + (j * 0x20), 0x292A);
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x292A);
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
__asm("nop");
|
__asm("nop");
|
||||||
if (p_rom[(j * 0x10) + 0x0F] == data)
|
if (p_rom[(j * (_flash_buffer_size >> 1)) + ((_flash_buffer_size >> 1) - 1)] == data)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -191,6 +243,33 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
|
|||||||
}
|
}
|
||||||
_FLASH_WRITE(address, 0xF0F0);
|
_FLASH_WRITE(address, 0xF0F0);
|
||||||
}
|
}
|
||||||
|
else if (_flash_type == 3)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < (int)(length / _flash_buffer_size); j++)
|
||||||
|
{
|
||||||
|
_FLASH_WRITE(0xAAA, 0xA9);
|
||||||
|
_FLASH_WRITE(0x555, 0x56);
|
||||||
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x26);
|
||||||
|
_FLASH_WRITE(address + (j * _flash_buffer_size), _flash_buffer_count);
|
||||||
|
u16 data = 0;
|
||||||
|
for (int i = 0; i < _flash_buffer_size; i += 2)
|
||||||
|
{
|
||||||
|
__asm("nop");
|
||||||
|
data = data_buffer[(j * _flash_buffer_size) + i + 1] << 8 | data_buffer[(j * _flash_buffer_size) + i];
|
||||||
|
_FLASH_WRITE(address + (j * _flash_buffer_size) + i, data);
|
||||||
|
}
|
||||||
|
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x2A);
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
__asm("nop");
|
||||||
|
if (p_rom[(j * (_flash_buffer_size >> 1)) + ((_flash_buffer_size >> 1) - 1)] == data)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_FLASH_WRITE(address, 0xF0);
|
||||||
|
}
|
||||||
|
|
||||||
REG_IE = ie;
|
REG_IE = ie;
|
||||||
}
|
}
|
||||||
@@ -215,14 +294,11 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
if (_flash_type == 0)
|
if (_flash_type == 0)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
// Temporarily store SRAM values at mapper registers
|
// Temporarily store SRAM values located at mapper registers
|
||||||
if (status.last_boot_save_type == SRAM_NONE)
|
sram_register_backup[0] = *(vu8 *)MAPPER_CONFIG1;
|
||||||
{
|
sram_register_backup[1] = *(vu8 *)MAPPER_CONFIG2;
|
||||||
sram_register_backup[0] = *(vu8 *)MAPPER_CONFIG1;
|
sram_register_backup[2] = *(vu8 *)MAPPER_CONFIG3;
|
||||||
sram_register_backup[1] = *(vu8 *)MAPPER_CONFIG2;
|
sram_register_backup[3] = *(vu8 *)MAPPER_CONFIG4;
|
||||||
sram_register_backup[2] = *(vu8 *)MAPPER_CONFIG3;
|
|
||||||
sram_register_backup[3] = *(vu8 *)MAPPER_CONFIG4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable SRAM access
|
// Enable SRAM access
|
||||||
*(vu8 *)MAPPER_CONFIG4 = 1;
|
*(vu8 *)MAPPER_CONFIG4 = 1;
|
||||||
@@ -243,16 +319,16 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
FlashEraseSector((_flash_save_block_offset + status.last_boot_save_index) * _flash_sector_size);
|
FlashEraseSector((_flash_save_block_offset + status.last_boot_save_index) * _flash_sector_size);
|
||||||
FlashWriteData((_flash_save_block_offset + status.last_boot_save_index) * _flash_sector_size, SRAM_SIZE);
|
FlashWriteData((_flash_save_block_offset + status.last_boot_save_index) * _flash_sector_size, SRAM_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save status to flash
|
|
||||||
status.last_boot_save_index = config.save_index;
|
|
||||||
status.last_boot_save_type = config.save_type;
|
|
||||||
memset((void *)data_buffer, 0, 0x1000);
|
|
||||||
memcpy(data_buffer, &status, sizeof(status));
|
|
||||||
FlashEraseSector(_flash_status_block_offset * flash_sector_size);
|
|
||||||
FlashWriteData(_flash_status_block_offset * flash_sector_size, 0x1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save status to flash
|
||||||
|
status.last_boot_save_index = config.save_index;
|
||||||
|
status.last_boot_save_type = config.save_type;
|
||||||
|
memset((void *)data_buffer, 0, 0x1000);
|
||||||
|
memcpy(data_buffer, &status, sizeof(status));
|
||||||
|
FlashEraseSector(_flash_status_block_offset * flash_sector_size);
|
||||||
|
FlashWriteData(_flash_status_block_offset * flash_sector_size, 0x1000);
|
||||||
|
|
||||||
// Disable SRAM access
|
// Disable SRAM access
|
||||||
*(vu8 *)MAPPER_CONFIG4 = 0;
|
*(vu8 *)MAPPER_CONFIG4 = 0;
|
||||||
|
|
||||||
@@ -278,9 +354,19 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
REG_IE = 0;
|
REG_IE = 0;
|
||||||
|
|
||||||
// Set mapper configuration
|
// Set mapper configuration
|
||||||
*(vu8 *)MAPPER_CONFIG1 = ((config.rom_offset / 0x40) & 0xF) << 4; // flash bank (0~7)
|
u8 mapper_config1 = ((config.rom_offset / 0x40) & 0xF) << 4; // flash bank (0~7)
|
||||||
*(vu8 *)MAPPER_CONFIG2 = 0x40 + (config.rom_offset % 0x40); // ROM offset (in 512 KB blocks) within current flash bank
|
u8 mapper_config2 = 0x40 + (config.rom_offset % 0x40); // ROM offset (in 512 KB blocks) within current flash bank
|
||||||
*(vu8 *)MAPPER_CONFIG3 = 0x40 - config.rom_size; // accessible ROM size (in 512 KB blocks)
|
u8 mapper_config3 = 0x40 - config.rom_size; // accessible ROM size (in 512 KB blocks)
|
||||||
|
u8 mapper_lock = mapper_config2 | 0x80;
|
||||||
|
|
||||||
|
*(vu8 *)MAPPER_CONFIG1 = mapper_config1;
|
||||||
|
__asm volatile("nop");
|
||||||
|
*(vu8 *)MAPPER_CONFIG2 = mapper_config2;
|
||||||
|
__asm volatile("nop");
|
||||||
|
*(vu8 *)MAPPER_CONFIG3 = mapper_config3;
|
||||||
|
__asm volatile("nop");
|
||||||
|
*(vu8 *)MAPPER_CONFIG2 = mapper_lock;
|
||||||
|
__asm volatile("nop");
|
||||||
|
|
||||||
// Wait until menu ROM is no longer visible
|
// Wait until menu ROM is no longer visible
|
||||||
u32 timeout = 0x2FFF;
|
u32 timeout = 0x2FFF;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ Author: Lesserkuma (github.com/lesserkuma)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#define MAGIC_FLASH_STATUS 0x414D554B
|
#define MAGIC_FLASH_STATUS 0x414D554B
|
||||||
#define FLASH_STATUS_LOCATION 0xA0000
|
|
||||||
|
|
||||||
typedef struct __attribute__((packed)) FlashStatus_
|
typedef struct __attribute__((packed)) FlashStatus_
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ extern s8 FontMarginBottom;
|
|||||||
extern const u8* font;
|
extern const u8* font;
|
||||||
extern u8 *itemlist;
|
extern u8 *itemlist;
|
||||||
extern u8 flash_type;
|
extern u8 flash_type;
|
||||||
|
extern u16 itemlist_offset;
|
||||||
extern u32 flash_sector_size;
|
extern u32 flash_sector_size;
|
||||||
|
extern u16 flash_buffer_size;
|
||||||
extern u32 flash_itemlist_sector_offset;
|
extern u32 flash_itemlist_sector_offset;
|
||||||
extern u32 flash_status_sector_offset;
|
extern u32 flash_status_sector_offset;
|
||||||
extern u32 flash_save_sector_offset;
|
extern u32 flash_save_sector_offset;
|
||||||
@@ -60,6 +62,7 @@ int main(void) {
|
|||||||
u8 redraw_items = 0xFF;
|
u8 redraw_items = 0xFF;
|
||||||
u8 roms_page = 7;
|
u8 roms_page = 7;
|
||||||
u16 kHeld = 0;
|
u16 kHeld = 0;
|
||||||
|
u16 kHeld_boot = 0;
|
||||||
BOOL show_debug = FALSE;
|
BOOL show_debug = FALSE;
|
||||||
BOOL show_credits = FALSE;
|
BOOL show_credits = FALSE;
|
||||||
BOOL boot_failed = FALSE;
|
BOOL boot_failed = FALSE;
|
||||||
@@ -90,9 +93,38 @@ int main(void) {
|
|||||||
SetMode(MODE_4 | BG2_ENABLE);
|
SetMode(MODE_4 | BG2_ENABLE);
|
||||||
dmaCopy(bgBitmap, (void*)AGB_VRAM+0xA000, SCREEN_WIDTH * SCREEN_HEIGHT);
|
dmaCopy(bgBitmap, (void*)AGB_VRAM+0xA000, SCREEN_WIDTH * SCREEN_HEIGHT);
|
||||||
|
|
||||||
|
// Check on-boot keys
|
||||||
|
scanKeys();
|
||||||
|
kHeld = keysHeld();
|
||||||
|
kHeld_boot = kHeld;
|
||||||
|
if ((kHeld & KEY_SELECT) && (kHeld & KEY_R)) {
|
||||||
|
show_credits = TRUE;
|
||||||
|
} else if (kHeld & KEY_SELECT) {
|
||||||
|
show_debug = TRUE;
|
||||||
|
}
|
||||||
|
if (kHeld) {
|
||||||
|
BOOL found_keys = FALSE;
|
||||||
|
for (itemlist_offset = 0; itemlist_offset < 0xE000; itemlist_offset += 0x70) {
|
||||||
|
memcpy(&sItemConfig, ((u8*)itemlist)+itemlist_offset, sizeof(sItemConfig));
|
||||||
|
if (sItemConfig.title_length == 0) break;
|
||||||
|
if (sItemConfig.title_length == 0xFF) break;
|
||||||
|
if (sItemConfig.keys == kHeld) {
|
||||||
|
found_keys = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found_keys) {
|
||||||
|
kHeld = 0;
|
||||||
|
itemlist_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Count number of ROMs
|
// Count number of ROMs
|
||||||
for (roms_total = 0; roms_total < 512; roms_total++) {
|
for (roms_total = 0; roms_total < 512; roms_total++) {
|
||||||
if ((itemlist[(0x70*roms_total+1)] == 0) || (itemlist[(0x70*roms_total+1)] == 0xFF)) break;
|
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset)+(0x70*roms_total), sizeof(sItemConfig));
|
||||||
|
if (sItemConfig.keys != kHeld) break;
|
||||||
|
if (sItemConfig.title_length == 0) break;
|
||||||
|
if (sItemConfig.title_length == 0xFF) break;
|
||||||
}
|
}
|
||||||
if (roms_total == 0) {
|
if (roms_total == 0) {
|
||||||
LoadFont(2);
|
LoadFont(2);
|
||||||
@@ -103,6 +135,10 @@ int main(void) {
|
|||||||
DrawText(14, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, u"No ROMs", 10, font, (void*)AGB_VRAM+0xA000, FALSE);
|
DrawText(14, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, u"No ROMs", 10, font, (void*)AGB_VRAM+0xA000, FALSE);
|
||||||
REG_DISPCNT ^= 0x0010;
|
REG_DISPCNT ^= 0x0010;
|
||||||
while (1) { VBlankIntrWait(); }
|
while (1) { VBlankIntrWait(); }
|
||||||
|
} else if (roms_total == 1) {
|
||||||
|
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset), sizeof(sItemConfig));
|
||||||
|
u8 error_code = BootGame(sItemConfig, sFlashStatus);
|
||||||
|
boot_failed = error_code;
|
||||||
}
|
}
|
||||||
page_total = (roms_total + 8.0 - 1) / 8.0;
|
page_total = (roms_total + 8.0 - 1) / 8.0;
|
||||||
|
|
||||||
@@ -119,15 +155,6 @@ int main(void) {
|
|||||||
page_active = sFlashStatus.last_boot_menu_index / 8;
|
page_active = sFlashStatus.last_boot_menu_index / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check on-boot keys
|
|
||||||
scanKeys();
|
|
||||||
kHeld = keysHeld();
|
|
||||||
if ((kHeld & KEY_SELECT) && (kHeld & KEY_R)) {
|
|
||||||
show_credits = TRUE;
|
|
||||||
} else if (kHeld & KEY_SELECT) {
|
|
||||||
show_debug = TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
s32 wait = 0;
|
s32 wait = 0;
|
||||||
u8 f = 0;
|
u8 f = 0;
|
||||||
while (1) {
|
while (1) {
|
||||||
@@ -143,9 +170,8 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
if (roms_page < 7) ClearList((void*)AGB_VRAM+0xA000, 26+(roms_page+1)*14, 14*(8-roms_page));
|
if (roms_page < 7) ClearList((void*)AGB_VRAM+0xA000, 26+(roms_page+1)*14, 14*(8-roms_page));
|
||||||
if (cursor_pos > roms_page) cursor_pos = roms_page;
|
if (cursor_pos > roms_page) cursor_pos = roms_page;
|
||||||
|
|
||||||
for (u8 i = 0; i <= roms_page; i++) {
|
for (u8 i = 0; i <= roms_page; i++) {
|
||||||
memcpy(&sItemConfig, ((u8*)itemlist)+0x70*(page_active*8+i), sizeof(sItemConfig));
|
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset)+0x70*(page_active*8+i), sizeof(sItemConfig));
|
||||||
ClearList((void*)AGB_VRAM+0xA000, 27+i*14, 14);
|
ClearList((void*)AGB_VRAM+0xA000, 27+i*14, 14);
|
||||||
LoadFont(sItemConfig.font);
|
LoadFont(sItemConfig.font);
|
||||||
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
|
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
|
||||||
@@ -154,7 +180,7 @@ int main(void) {
|
|||||||
// Re-draw only changed list items (cursor moved up or down)
|
// Re-draw only changed list items (cursor moved up or down)
|
||||||
for (u8 i = 0; i < 8; i++) {
|
for (u8 i = 0; i < 8; i++) {
|
||||||
if ((redraw_items >> i) & 1) {
|
if ((redraw_items >> i) & 1) {
|
||||||
memcpy(&sItemConfig, ((u8*)itemlist)+0x70*(page_active*8+i), sizeof(sItemConfig));
|
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset)+0x70*(page_active*8+i), sizeof(sItemConfig));
|
||||||
ClearList((void*)AGB_VRAM+0xA000, 27+i*14, 14);
|
ClearList((void*)AGB_VRAM+0xA000, 27+i*14, 14);
|
||||||
LoadFont(sItemConfig.font);
|
LoadFont(sItemConfig.font);
|
||||||
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
|
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
|
||||||
@@ -162,8 +188,8 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(&sItemConfig, ((u8*)itemlist)+0x70*(page_active*8+cursor_pos), sizeof(sItemConfig));
|
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset)+0x70*(page_active*8+cursor_pos), sizeof(sItemConfig));
|
||||||
|
|
||||||
// Draw cursor
|
// Draw cursor
|
||||||
LoadFont(1);
|
LoadFont(1);
|
||||||
ClearList((void*)AGB_VRAM+0xA000, SCREEN_HEIGHT - sFontSpecs.max_height - 1, sFontSpecs.max_height);
|
ClearList((void*)AGB_VRAM+0xA000, SCREEN_HEIGHT - sFontSpecs.max_height - 1, sFontSpecs.max_height);
|
||||||
@@ -173,7 +199,7 @@ int main(void) {
|
|||||||
// Draw status bar
|
// Draw status bar
|
||||||
LoadFont(2);
|
LoadFont(2);
|
||||||
memset(temp_unicode, 0, sizeof(temp_unicode));
|
memset(temp_unicode, 0, sizeof(temp_unicode));
|
||||||
snprintf(temp_ascii, 10+1, "%d/%d", page_active*8+cursor_pos+1, roms_total);
|
snprintf(temp_ascii, 10+1, "%d/%d", page_active+1, page_total);
|
||||||
AsciiToUnicode(temp_ascii, temp_unicode);
|
AsciiToUnicode(temp_ascii, temp_unicode);
|
||||||
DrawText(11, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, temp_unicode, 10, font, (void*)AGB_VRAM+0xA000, FALSE);
|
DrawText(11, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, temp_unicode, 10, font, (void*)AGB_VRAM+0xA000, FALSE);
|
||||||
if (boot_failed) {
|
if (boot_failed) {
|
||||||
@@ -196,7 +222,7 @@ int main(void) {
|
|||||||
u8 a = ((sItemConfig.rom_offset / 0x40) & 0xF) << 4;
|
u8 a = ((sItemConfig.rom_offset / 0x40) & 0xF) << 4;
|
||||||
u8 b = 0x40 + (sItemConfig.rom_offset % 0x40);
|
u8 b = 0x40 + (sItemConfig.rom_offset % 0x40);
|
||||||
u8 c = 0x40 - sItemConfig.rom_size;
|
u8 c = 0x40 - sItemConfig.rom_size;
|
||||||
snprintf(temp_ascii, 64, "%02X:%02X:%02X|0x%X~%dMiB|%X", a, b, c, (int)(sItemConfig.rom_offset * 512 * 1024), (int)(sItemConfig.rom_size * 512 >> 10), (int)(flash_save_sector_offset + sItemConfig.save_index));
|
snprintf(temp_ascii, 64, "%02X:%02X:%02X|0x%X~%dMiB|%X - %X", a, b, c, (unsigned int)(sItemConfig.rom_offset * 512 * 1024), (int)(sItemConfig.rom_size * 512 >> 10), (unsigned int)(flash_save_sector_offset + sItemConfig.save_index), (unsigned int)flash_buffer_size);
|
||||||
memset(temp_unicode, 0, sizeof(temp_unicode));
|
memset(temp_unicode, 0, sizeof(temp_unicode));
|
||||||
AsciiToUnicode(temp_ascii, temp_unicode);
|
AsciiToUnicode(temp_ascii, temp_unicode);
|
||||||
DrawText(6, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, temp_unicode, 64, font, (void*)AGB_VRAM+0xA000, FALSE);
|
DrawText(6, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, temp_unicode, 64, font, (void*)AGB_VRAM+0xA000, FALSE);
|
||||||
@@ -212,6 +238,11 @@ int main(void) {
|
|||||||
// Check for menu keys
|
// Check for menu keys
|
||||||
scanKeys();
|
scanKeys();
|
||||||
kHeld = keysHeld();
|
kHeld = keysHeld();
|
||||||
|
if ((kHeld_boot == 0) || (kHeld != kHeld_boot)) {
|
||||||
|
kHeld_boot = 0;
|
||||||
|
} else {
|
||||||
|
kHeld = 0;
|
||||||
|
}
|
||||||
if (kHeld != 0) {
|
if (kHeld != 0) {
|
||||||
wait++;
|
wait++;
|
||||||
} else {
|
} else {
|
||||||
@@ -233,13 +264,15 @@ int main(void) {
|
|||||||
sFlashStatus.last_boot_menu_index = page_active * 8 + cursor_pos;
|
sFlashStatus.last_boot_menu_index = page_active * 8 + cursor_pos;
|
||||||
if (!show_credits && !show_debug) {
|
if (!show_credits && !show_debug) {
|
||||||
LoadFont(0);
|
LoadFont(0);
|
||||||
if (sFlashStatus.battery_present == 1) {
|
DrawText(5, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, u"Loading… Don't turn off the power!", 48, font, (void*)AGB_VRAM+0xA000, FALSE);
|
||||||
DrawText(5, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, u"Loading… Don't turn off the power!", 48, font, (void*)AGB_VRAM+0xA000, FALSE);
|
|
||||||
}
|
|
||||||
REG_DISPCNT ^= 0x0010;
|
REG_DISPCNT ^= 0x0010;
|
||||||
dmaCopy((void*)AGB_VRAM+0xA000, (void*)AGB_VRAM, SCREEN_WIDTH * SCREEN_HEIGHT);
|
dmaCopy((void*)AGB_VRAM+0xA000, (void*)AGB_VRAM, SCREEN_WIDTH * SCREEN_HEIGHT);
|
||||||
REG_DISPCNT ^= 0x0010;
|
REG_DISPCNT ^= 0x0010;
|
||||||
}
|
}
|
||||||
|
if (kHeld & KEY_SELECT) {
|
||||||
|
// Skips reading latest save data from SRAM
|
||||||
|
sFlashStatus.last_boot_save_type = SRAM_NONE;
|
||||||
|
}
|
||||||
u8 error_code = BootGame(sItemConfig, sFlashStatus);
|
u8 error_code = BootGame(sItemConfig, sFlashStatus);
|
||||||
boot_failed = error_code;
|
boot_failed = error_code;
|
||||||
redraw_items = 0xFF;
|
redraw_items = 0xFF;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ typedef struct ItemConfig_
|
|||||||
u16 rom_size;
|
u16 rom_size;
|
||||||
SAVE_TYPE save_type;
|
SAVE_TYPE save_type;
|
||||||
u8 save_index;
|
u8 save_index;
|
||||||
u16 index;
|
u16 keys;
|
||||||
u8 reserved[6];
|
u8 reserved[6];
|
||||||
u16 title[0x30];
|
u16 title[0x30];
|
||||||
} ItemConfig;
|
} ItemConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user