23 Commits
0.1 ... main

Author SHA1 Message Date
Lesserkuma
2a7b04e1ee 1.2 2026-07-06 15:21:42 +02:00
Lesserkuma
3e0a6e89cb 1.1 2024-02-16 13:55:23 +01:00
Lesserkuma
89b22c8205 1.0 2024-02-07 16:12:44 +01:00
Lesserkuma
39ab8936ec - 2024-01-06 19:27:44 +01:00
Lesserkuma
a2fa155e8e fixes 2024-01-06 14:16:48 +01:00
Lesserkuma
f756a705ae New config option + autoboot if there is only one game in the list 2024-01-06 11:47:03 +01:00
Lesserkuma
d51437552e - 2023-12-17 01:03:38 +01:00
Lesserkuma
12d953d8fd - 2023-12-17 00:46:37 +01:00
Lesserkuma
1bf4927d6b Support for MSP54LV100 2023-11-24 18:05:40 +01:00
Lesserkuma
a3c4a44893 0.7 2023-11-01 20:36:02 +01:00
Lesserkuma
cbe17ef7c5 0.6 2023-09-24 12:54:20 +02:00
Lesserkuma
8dc1d10a95 0.5 2023-08-17 17:57:35 +02:00
Lesserkuma
bcb9ae5d65 - 2023-08-01 12:47:35 +02:00
Lesserkuma
a33f0ed9db 0.3 2023-07-31 14:10:20 +02:00
Lesserkuma
34b1b4b8dc - 2023-07-28 16:29:42 +02:00
Lesserkuma
e21ad43d20 - 2023-07-28 13:52:49 +02:00
Lesserkuma
8f63cdb03f - 2023-07-27 19:40:18 +02:00
Lesserkuma
9499327b3b - 2023-07-27 19:21:22 +02:00
Lesserkuma
2c88286a66 - 2023-07-27 19:12:28 +02:00
Lesserkuma
ce1212d365 - 2023-07-26 18:01:49 +02:00
Lesserkuma
8cd4c1b953 - 2023-07-26 17:25:13 +02:00
Lesserkuma
d8df1ff28e - 2023-07-26 17:24:32 +02:00
Lesserkuma
b43c23db22 - 2023-07-26 17:11:16 +02:00
12 changed files with 620 additions and 140 deletions

BIN
.github/screen.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

179
.github/workflows/build.yaml vendored Normal file
View 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

View File

@@ -179,10 +179,13 @@ soundbank.bin soundbank.h : $(AUDIOFILES)
#replacement rule for gbafix
#---------------------------------------------------------------------------------
$(shell touch $(CURDIR)/../$(SOURCES)/version.h)
%.gba: %.elf
@$(OBJCOPY) -O binary $< $@
@gbafix $@ "-tLK MULTIMENU" "-cAGBJ" "-mLK" "-r0"
@echo Copying to ROM builder folder
@cp $@ "$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))/rom_builder/lk_multimenu.gba"
@echo Done!
#---------------------------------------------------------------------------------------
endif

View File

@@ -13,18 +13,34 @@ Open the config.json file in a text editor like Notepad.
The following section must be edited in order to specify the cartridge type to use and whether or not your cartridge has a battery installed:
```json
"cartridge": {
"type": 2,
"battery_present": false
},
"cartridge": {
"type": 2,
"battery_present": false,
"min_rom_size": 4194304
},
```
Set `type` to `1` or `2`:
- `1` = MSP55LV100S (e.g. The Legend of Zelda Collection - Classic Edition 7-in-1)
- `2` = 6600M0U0BE (e.g. 369IN1 2048M)
Set `type` to one of these:
- `1` = MSP55LV100S (e.g. The Legend of Zelda Collection - Classic Edition 7-in-1, 64 MiB)
- `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 `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:
```json
"games": [
{
"enabled": true,
"file": "wah7.gba",
"title": "Super WAHluigi Bros. 7",
"title_font": 1,
"save_slot": 1
},
```
- `enabled` can be set to `true` or `false`. If this option is set, the game entry will be skipped by the ROM Builder.
- `file` is the ROM's file name within the **roms** folder, including file extension.
- `title` is the unicode title that will be displayed in the menu.
@@ -36,6 +52,8 @@ In the `games` section, you can edit the game-related stuff:
- `5` = Nintendo DSi IPL font (KOR)
- `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.
- `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
@@ -46,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-log don't write a log file
--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
```
@@ -56,13 +75,23 @@ No command line arguments are required for creating a compilation, however there
- up to 64 KiB of save data per ROM
### 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
Tested repro cartridges:
- 100BS6600_48BALL_V4 with 6600M0U0BE
- 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
ROM and save data 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 to FraX, Ausar, liuyunx, BennVenn, Jenetrix, Matt
## Screenshots
<img src="https://raw.githubusercontent.com/lesserkuma/GBA_MultiMenu/master/.github/screen.png" alt="" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 756 B

BIN
rom_builder/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -5,7 +5,7 @@
import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime
# Configuration
app_version = "0.1"
app_version = "1.2"
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] = c.upper()
def formatFileSize(size):
if size == 1:
return "{:d} Byte".format(size)
@@ -39,16 +38,36 @@ def logp(*args, **kwargs):
################################
cartridge_types = [
{ # "MSP55LV100S
{
"name":"MSP55LV100S",
"flash_size":0x4000000,
"sector_size":0x20000,
"block_size":0x80000,
},
{ # 6600M0U0BE
{
"name":"6600M0U0BE",
"flash_size":0x10000000,
"sector_size":0x40000,
"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()
log = ""
@@ -60,15 +79,16 @@ parser.add_argument("--split", help="splits output files into 32 MiB parts", act
parser.add_argument("--no-wait", help="dont wait for user input when finished", action="store_true", default=False)
parser.add_argument("--no-log", help="dont 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("--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")
args = parser.parse_args()
output_file = args.output
if output_file == "lk_multimenu.gba":
logp("Error: The file must not be named lk_multimenu.gba")
logp("Error: The file must not be named lk_multimenu.gba")
if not args.no_wait: input("\nPress ENTER to exit.\n")
sys.exit(1)
if not os.path.exists("lk_multimenu.gba"):
logp("Error: The Menu ROM is missing.\nPlease put it in the same directory that you are running this tool from.\nExpected file name: \"lk_multimenu.gba\"")
logp("Error: The Menu ROM is missing.\nPlease put it in the same directory that you are running this tool from.\nExpected file name: lk_multimenu.gba")
if not args.no_wait: input("\nPress ENTER to exit.\n")
sys.exit()
@@ -81,30 +101,53 @@ if not os.path.exists(args.config):
games = []
cartridge_type = 1
battery_present = False
min_rom_size = 0x400000
for file in files:
games.append({
d = {
"enabled": True,
"file": os.path.split(file)[1],
"title": os.path.splitext(os.path.split(file)[1])[0],
"title_font": 1,
"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
obj = {
"cartridge": {
"type": cartridge_type + 1,
"battery_present": battery_present,
"min_rom_size": min_rom_size,
},
"games": games,
}
with open(args.config, "w", encoding="UTF-8-SIG") as f:
f.write(json.dumps(obj=obj, indent=4, ensure_ascii=False))
if len(games) == 0:
logp("Error: No usable ROM files were found in the “roms” folder.")
else:
with open(args.config, "w", encoding="UTF-8-SIG") as f:
f.write(json.dumps(obj=obj, indent=4, ensure_ascii=False))
logp(f"A new configuration file ({args.config:s}) was created based on the files inside the “roms” folder.\nPlease edit the file to your liking in a text editor, then run this tool again.")
if not args.no_wait: input("\nPress ENTER to exit.\n")
sys.exit()
else:
with open(args.config, "r", encoding="UTF-8-SIG") as f:
j = json.load(f)
try:
j = json.load(f)
except json.decoder.JSONDecodeError as e:
logp(f"Error: The configuration file ({args.config:s}) is malformed and could not be loaded.\n" + str(e))
if not args.no_wait: input("\nPress ENTER to exit.\n")
sys.exit()
games = j["games"]
cartridge_type = j["cartridge"]["type"] - 1
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
flash_size = cartridge_types[cartridge_type]["flash_size"]
@@ -114,6 +157,7 @@ block_size = cartridge_types[cartridge_type]["block_size"]
block_count = flash_size // block_size
sectors_per_block = 0x80000 // sector_size
compilation = bytearray()
roms_keys = [0]
for i in range(flash_size // 0x2000000):
chunk = bytearray([0xFF] * 0x2000000)
compilation += chunk
@@ -121,22 +165,52 @@ sector_map = list("." * sector_count)
# Read menu ROM
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: Couldnt update background image. Pillow library is not installed.")
menu_rom_size = menu_rom.find(b"dkARM\0\0\0") + 8
compilation[0:len(menu_rom)] = menu_rom
UpdateSectorMap(start=0, length=math.ceil(len(menu_rom) / sector_size), c="m")
item_list_offset = len(menu_rom)
item_list_offset = 0x40000 - (item_list_offset % 0x40000) + item_list_offset
item_list_offset = math.ceil(item_list_offset / sector_size)
UpdateSectorMap(start=item_list_offset, length=1, c="i")
UpdateSectorMap(start=item_list_offset, length=1, c="l")
status_offset = item_list_offset + 1
UpdateSectorMap(start=status_offset, length=1, c="c")
if battery_present:
status = bytearray([0x4B, 0x55, 0x4D, 0x41, 0x00, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
status = bytearray([0x4B, 0x55, 0x4D, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
else:
status = bytearray([0x4B, 0x55, 0x4D, 0x41, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
status = bytearray([0x4B, 0x55, 0x4D, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
compilation[status_offset * sector_size:status_offset * sector_size + len(status)] = status
save_data_sector_offset = status_offset + 1
boot_logo_found = hashlib.sha1(compilation[0x04:0xA0]).digest() == bytearray([ 0x17, 0xDA, 0xA0, 0xFE, 0xC0, 0x2F, 0xC3, 0x3C, 0x0F, 0x6A, 0xBB, 0x54, 0x9A, 0x8B, 0x80, 0xB6, 0x61, 0x3B, 0x48, 0xEE ])
# Read game ROMs and import save data
saves_read = []
@@ -152,6 +226,13 @@ for game in games:
x = 0x80000
while (x < size): x *= 2
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["size"] = size
if "title_font" in game:
@@ -159,7 +240,37 @@ for game in games:
else:
game["title_font"] = 0
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:
game["save_type"] = 2
game["save_slot"] -= 1
@@ -190,7 +301,7 @@ else:
games = [game for game in games if not ("missing" in game and game["missing"])]
if len(games) == 0:
logp("No ROMs found")
logp(f"No ROMs found. Delete the “{args.config:s}” file to reset your configuration.")
sys.exit()
# Add index
@@ -201,19 +312,23 @@ for game in games:
# Read ROM data
games.sort(key=lambda game: game["size"], reverse=True)
boot_logo_found = False
c = 0
for game in games:
found = False
for i in range(save_end_offset, len(sector_map)):
if i % game["sector_count"] == 0:
sector_count_map = game["sector_count"]
if "map_256m" in game and game["map_256m"] == True:
# Map as 256M ROM, but don't waste space; some games may need this for unknown reasons
sector_count_map = (32 * 1024 * 1024) // sector_size
if i % sector_count_map == 0:
if sector_map[i:i + game["sector_count"]] == ["."] * game["sector_count"]:
UpdateSectorMap(i, game["sector_count"], "r")
with open(f"roms/{game['file']}", "rb") as f: rom = f.read()
compilation[i * sector_size:i * sector_size + len(rom)] = rom
game["sector_offset"] = i
game["block_offset"] = game["sector_offset"] * sector_size // block_size
game["block_count"] = game["sector_count"] * sector_size // block_size
game["block_count"] = sector_count_map * sector_size // block_size
found = True
if not boot_logo_found and hashlib.sha1(rom[0x04:0xA0]).digest() == bytearray([ 0x17, 0xDA, 0xA0, 0xFE, 0xC0, 0x2F, 0xC3, 0x3C, 0x0F, 0x6A, 0xBB, 0x54, 0x9A, 0x8B, 0x80, 0xB6, 0x61, 0x3B, 0x48, 0xEE ]):
@@ -240,41 +355,52 @@ logp("{:.2f}% ({:d} of {:d} sectors) used\n".format(sectors_used / sector_count
logp(f"Added {len(games)} ROM(s) to the compilation\n")
if battery_present:
logp (" | Offset | Size | Save Slot | Title")
toc_sep = "----+-----------+-----------+----------------+---------------------------------"
logp (" | Offset | Map Size | Save Slot | Title")
toc_sep = "----+------------+-----------+----------------+--------------------------------"
else:
logp (" | Offset | Size | Title")
toc_sep = "----+-----------+-----------+--------------------------------------------------"
logp (" | Offset | Map Size | Title")
toc_sep = "----+------------+-----------+-------------------------------------------------"
item_list = bytearray()
for game in games:
title = game["title"]
if len(title) > 0x30: title = title[:0x2F] + ""
table_line = \
f"{game['index'] + 1:3d} | " \
f"0x{game['block_offset'] * block_size:07X} | "\
f"0x{game['block_count'] * block_size:07X} | "
if battery_present:
if game['save_type'] > 0:
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
else:
table_line += "-------------- | "
table_line += f"{title}"
if c % 8 == 0: logp(toc_sep)
logp(table_line)
c += 1
#logp(f"0x{game['block_offset'] * block_size:07X} |")
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([0] * 8)
item_list += bytearray(title.encode("UTF-16LE"))
for key in roms_keys:
c = 0
for game in games:
if game["keys"] != key: continue
title = game["title"]
if len(title) > 0x30: title = title[:0x2F] + ""
table_line = \
f"{game['index'] + 1:3d} | " + \
f"0x{game['block_offset'] * block_size:X} | ".rjust(13, " ") + \
f"0x{game['block_count'] * block_size:X} | ".rjust(12, " ")
if battery_present:
if game['save_type'] > 0:
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
else:
table_line += " | "
table_line += f"{title}"
if c % 8 == 0:
if game['keys'] != 0:
temp = toc_sep[:-9] + "[Hidden]-"
logp(temp)
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
rom_code = "L{:s}".format(hashlib.sha1(status + item_list).hexdigest()[:3]).upper()
@@ -287,16 +413,25 @@ for i in range(0xA0, 0xBD):
checksum = (checksum - 0x19) & 0xFF
compilation[0xBD] = checksum
logp("")
logp("Compilation ROM size: {:.2f} MiB".format(rom_size / 1024 / 1024))
logp("ROM code: {:s}".format(rom_code))
logp("Menu ROM: 0x{:08X}0x{:08X}".format(0, len(menu_rom)))
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{:08X}0x{:08X}".format(status_offset * sector_size, status_offset * sector_size + 0x1000))
logp("")
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 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)
if args.split:
for i in range(0, math.ceil(flash_size / 0x2000000)):
pos = i * 0x2000000
size = 0x2000000
if pos > len(compilation[:rom_size]): break
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])
else:
with open(output_file, "wb") as f: f.write(compilation[:rom_size])
@@ -307,7 +442,3 @@ if not args.no_log:
log += "\n################################\n\n"
with open("log.txt", "ab") as f: f.write(log.encode("UTF-8-SIG"))
if not args.no_wait: input("\nPress ENTER to exit.\n")
# Debug
#with open("output_menu.gba", "wb") as f: f.write(compilation[:save_data_sector_offset * sector_size])
#with open("output_menu+save.gba", "wb") as f: f.write(compilation[:save_end_offset * sector_size])

View File

@@ -12,7 +12,9 @@ Author: Lesserkuma (github.com/lesserkuma)
u8 flash_type;
u8 *itemlist;
u16 itemlist_offset;
u32 flash_sector_size;
u16 flash_buffer_size;
u32 flash_itemlist_sector_offset;
u32 flash_status_sector_offset;
u32 flash_save_sector_offset;
@@ -46,6 +48,7 @@ IWRAM_CODE void FlashDetectType(void)
REG_IE = ie;
flash_type = 1;
flash_sector_size = 0x40000;
flash_buffer_size = 0x400;
FlashCalcOffsets();
return;
}
@@ -62,6 +65,35 @@ IWRAM_CODE void FlashDetectType(void)
REG_IE = ie;
flash_type = 2;
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();
return;
}
@@ -119,6 +151,24 @@ IWRAM_CODE void FlashEraseSector(u32 address)
}
_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;
}
@@ -130,33 +180,35 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
FlashDetectType();
}
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 ie = REG_IE;
REG_IE = ie & 0xFFFE;
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)
{
__asm("nop");
if ((p_rom[(j * 0x200)] & 0x80) == 0x80)
if ((p_rom[(j * (_flash_buffer_size >> 1))] & 0x80) == 0x80)
{
break;
}
}
_FLASH_WRITE(address + (j * 0x400), 0x1FF);
for (int i = 0; i < 0x400; i += 2)
_FLASH_WRITE(address + (j * _flash_buffer_size), _flash_buffer_count);
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)
{
__asm("nop");
if ((p_rom[(j * 0x200)] & 0x80) == 0x80)
if ((p_rom[(j * (_flash_buffer_size >> 1))] & 0x80) == 0x80)
{
break;
}
@@ -166,24 +218,24 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
}
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(0x555, 0x5556);
_FLASH_WRITE(address + (j * 0x20), 0x2526);
_FLASH_WRITE(address + (j * 0x20), 0x0F0F);
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x2526);
_FLASH_WRITE(address + (j * _flash_buffer_size), _flash_buffer_count | (_flash_buffer_count << 8));
u16 data = 0;
for (int i = 0; i < 0x20; i += 2)
for (int i = 0; i < _flash_buffer_size; i += 2)
{
__asm("nop");
data = data_buffer[(j * 0x20) + i + 1] << 8 | data_buffer[(j * 0x20) + i];
_FLASH_WRITE(address + (j * 0x20) + i, data);
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 * 0x20), 0x292A);
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x292A);
while (1)
{
__asm("nop");
if (p_rom[(j * 0x10) + 0x0F] == data)
if (p_rom[(j * (_flash_buffer_size >> 1)) + ((_flash_buffer_size >> 1) - 1)] == data)
{
break;
}
@@ -191,6 +243,33 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
}
_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;
}
@@ -215,7 +294,7 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
if (_flash_type == 0)
return 1;
// Temporarily store SRAM values at mapper registers
// Temporarily store SRAM values located at mapper registers
sram_register_backup[0] = *(vu8 *)MAPPER_CONFIG1;
sram_register_backup[1] = *(vu8 *)MAPPER_CONFIG2;
sram_register_backup[2] = *(vu8 *)MAPPER_CONFIG3;
@@ -225,7 +304,8 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
*(vu8 *)MAPPER_CONFIG4 = 1;
// Write previous SRAM to flash
if (status.battery_present) {
if (status.battery_present)
{
if (status.last_boot_save_type != SRAM_NONE)
{
for (int i = 0; i < SRAM_SIZE; i++)
@@ -239,16 +319,16 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
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);
}
// 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
*(vu8 *)MAPPER_CONFIG4 = 0;
@@ -274,12 +354,22 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
REG_IE = 0;
// Set mapper configuration
*(vu8 *)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
*(vu8 *)MAPPER_CONFIG3 = 0x40 - config.rom_size; // accessible ROM size (in 512 KB blocks)
u8 mapper_config1 = ((config.rom_offset / 0x40) & 0xF) << 4; // flash bank (0~7)
u8 mapper_config2 = 0x40 + (config.rom_offset % 0x40); // ROM offset (in 512 KB blocks) within current flash bank
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
u16 timeout = 0xFFFF;
u32 timeout = 0x2FFF;
while (((vu16 *)AGB_ROM)[0x58] == 0x4B4C)
{
if (!timeout--)
@@ -294,16 +384,20 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
// Lock mapper
*(vu8 *)MAPPER_CONFIG2 |= 0x80;
// Restore SRAM values at mapper registers
*(vu8 *)MAPPER_CONFIG1 = sram_register_backup[0];
*(vu8 *)MAPPER_CONFIG2 = sram_register_backup[1];
*(vu8 *)MAPPER_CONFIG3 = sram_register_backup[2];
*(vu8 *)MAPPER_CONFIG4 = sram_register_backup[3];
// Write buffer to SRAM
for (int i = 0; i < SRAM_SIZE; i++)
if (config.save_type != SRAM_NONE)
{
((vu8 *)AGB_SRAM)[i] = data_buffer[i];
for (int i = 0; i < SRAM_SIZE; i++)
{
((vu8 *)AGB_SRAM)[i] = data_buffer[i];
}
}
else
{
*(vu8 *)MAPPER_CONFIG1 = sram_register_backup[0];
*(vu8 *)MAPPER_CONFIG2 = sram_register_backup[1];
*(vu8 *)MAPPER_CONFIG3 = sram_register_backup[2];
*(vu8 *)MAPPER_CONFIG4 = sram_register_backup[3];
}
// Clear palette
@@ -313,7 +407,7 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
}
REG_BLDY = 0;
// Soft Reset system call
// Boot ROM
__asm("swi 0"); // Soft reset
return 3;

View File

@@ -15,7 +15,6 @@ Author: Lesserkuma (github.com/lesserkuma)
}
#define MAGIC_FLASH_STATUS 0x414D554B
#define FLASH_STATUS_LOCATION 0xA0000
typedef struct __attribute__((packed)) FlashStatus_
{

View File

@@ -14,6 +14,7 @@ Author: Lesserkuma (github.com/lesserkuma)
#include <string.h>
#include <unistd.h>
#include "version.h"
#include "main.h"
#include "font.h"
#include "flash.h"
@@ -26,7 +27,9 @@ extern s8 FontMarginBottom;
extern const u8* font;
extern u8 *itemlist;
extern u8 flash_type;
extern u16 itemlist_offset;
extern u32 flash_sector_size;
extern u16 flash_buffer_size;
extern u32 flash_itemlist_sector_offset;
extern u32 flash_status_sector_offset;
extern u32 flash_save_sector_offset;
@@ -59,6 +62,7 @@ int main(void) {
u8 redraw_items = 0xFF;
u8 roms_page = 7;
u16 kHeld = 0;
u16 kHeld_boot = 0;
BOOL show_debug = FALSE;
BOOL show_credits = FALSE;
BOOL boot_failed = FALSE;
@@ -89,9 +93,38 @@ int main(void) {
SetMode(MODE_4 | BG2_ENABLE);
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
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) {
LoadFont(2);
@@ -102,11 +135,15 @@ int main(void) {
DrawText(14, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, u"No ROMs", 10, font, (void*)AGB_VRAM+0xA000, FALSE);
REG_DISPCNT ^= 0x0010;
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;
memcpy(&sFlashStatus, (void *)(AGB_ROM + flash_status_sector_offset * flash_sector_size), sizeof(sFlashStatus));
if ((sFlashStatus.magic != MAGIC_FLASH_STATUS) || (sFlashStatus.last_boot_menu_index > roms_total)) {
if ((sFlashStatus.magic != MAGIC_FLASH_STATUS) || (sFlashStatus.last_boot_menu_index >= roms_total)) {
sFlashStatus.magic = MAGIC_FLASH_STATUS;
sFlashStatus.version = 0;
sFlashStatus.battery_present = 1;
@@ -118,15 +155,6 @@ int main(void) {
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;
u8 f = 0;
while (1) {
@@ -142,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 (cursor_pos > roms_page) cursor_pos = roms_page;
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);
LoadFont(sItemConfig.font);
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
@@ -153,7 +180,7 @@ int main(void) {
// Re-draw only changed list items (cursor moved up or down)
for (u8 i = 0; i < 8; i++) {
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);
LoadFont(sItemConfig.font);
DrawText(28, 26+i*14, ALIGN_LEFT, sItemConfig.title, sItemConfig.title_length, font, (void*)AGB_VRAM+0xA000, i == cursor_pos);
@@ -161,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
LoadFont(1);
ClearList((void*)AGB_VRAM+0xA000, SCREEN_HEIGHT - sFontSpecs.max_height - 1, sFontSpecs.max_height);
@@ -172,7 +199,7 @@ int main(void) {
// Draw status bar
LoadFont(2);
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);
DrawText(11, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_RIGHT, temp_unicode, 10, font, (void*)AGB_VRAM+0xA000, FALSE);
if (boot_failed) {
@@ -186,20 +213,16 @@ int main(void) {
}
} else if (show_credits) {
LoadFont(0);
#ifdef __TIMESTAMP_ISO__
memset(temp_unicode, 0, sizeof(temp_unicode));
snprintf(temp_ascii, 48, "Menu by LK - %s", __TIMESTAMP_ISO__);
AsciiToUnicode(temp_ascii, temp_unicode);
DrawText(6, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, temp_unicode, 48, font, (void*)AGB_VRAM+0xA000, FALSE);
#else
DrawText(6, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, u"Menu by LK", 48, font, (void*)AGB_VRAM+0xA000, FALSE);
#endif
memset(temp_unicode, 0, sizeof(temp_unicode));
snprintf(temp_ascii, 48, "Menu by LK - %s", BUILDTIME);
AsciiToUnicode(temp_ascii, temp_unicode);
DrawText(6, SCREEN_HEIGHT - sFontSpecs.max_height - 3 - FontMarginBottom, ALIGN_LEFT, temp_unicode, 48, font, (void*)AGB_VRAM+0xA000, FALSE);
} else if (show_debug) {
LoadFont(0);
u8 a = ((sItemConfig.rom_offset / 0x40) & 0xF) << 4;
u8 b = 0x40 + (sItemConfig.rom_offset % 0x40);
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));
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);
@@ -215,6 +238,11 @@ int main(void) {
// Check for menu keys
scanKeys();
kHeld = keysHeld();
if ((kHeld_boot == 0) || (kHeld != kHeld_boot)) {
kHeld_boot = 0;
} else {
kHeld = 0;
}
if (kHeld != 0) {
wait++;
} else {
@@ -236,13 +264,15 @@ int main(void) {
sFlashStatus.last_boot_menu_index = page_active * 8 + cursor_pos;
if (!show_credits && !show_debug) {
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;
dmaCopy((void*)AGB_VRAM+0xA000, (void*)AGB_VRAM, SCREEN_WIDTH * SCREEN_HEIGHT);
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);
boot_failed = error_code;
redraw_items = 0xFF;

View File

@@ -66,7 +66,7 @@ typedef struct ItemConfig_
u16 rom_size;
SAVE_TYPE save_type;
u8 save_index;
u16 index;
u16 keys;
u8 reserved[6];
u16 title[0x30];
} ItemConfig;

15
source/version.h Normal file
View File

@@ -0,0 +1,15 @@
/*
GBA Multi Game Menu
Author: Lesserkuma (github.com/lesserkuma)
*/
#ifndef VERSION_H_
#define VERSION_H_
#ifdef __TIMESTAMP_ISO__
#define BUILDTIME __TIMESTAMP_ISO__
#else
#define BUILDTIME __TIMESTAMP__
#endif
#endif