2 Commits
1.0 ... 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
5 changed files with 265 additions and 49 deletions

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

@@ -19,11 +19,12 @@ The following section must be edited in order to specify the cartridge type to u
"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, 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.
@@ -74,9 +75,7 @@ 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 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/).
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.
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.
@@ -86,8 +85,9 @@ Tested repro cartridges:
- 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 to FraX, Ausar, liuyunx, BennVenn, Jenetrix, Matt

View File

@@ -5,7 +5,7 @@
import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime
# Configuration
app_version = "1.0"
app_version = "1.2"
default_file = "LK_MULTIMENU_<CODE>.gba"
################################
@@ -62,6 +62,12 @@ cartridge_types = [
"sector_size":0x40000,
"block_size":0x80000,
},
{
"name":"GL04GR00FHCR2",
"flash_size":0x10000000,
"sector_size":0x20000,
"block_size":0x80000,
},
]
now = datetime.datetime.now()
log = ""
@@ -414,6 +420,9 @@ 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:

View File

@@ -14,6 +14,7 @@ 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;
@@ -47,6 +48,7 @@ IWRAM_CODE void FlashDetectType(void)
REG_IE = ie;
flash_type = 1;
flash_sector_size = 0x40000;
flash_buffer_size = 0x400;
FlashCalcOffsets();
return;
}
@@ -63,11 +65,13 @@ 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);
@@ -76,9 +80,20 @@ IWRAM_CODE void FlashDetectType(void)
_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;
}
@@ -165,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;
}
@@ -201,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;
}
@@ -228,24 +245,24 @@ IWRAM_CODE void FlashWriteData(u32 address, u32 length)
}
else if (_flash_type == 3)
{
for (int j = 0; j < (int)(length / 0x40); j++)
for (int j = 0; j < (int)(length / _flash_buffer_size); j++)
{
_FLASH_WRITE(0xAAA, 0xA9);
_FLASH_WRITE(0x555, 0x56);
_FLASH_WRITE(address + (j * 0x40), 0x26);
_FLASH_WRITE(address + (j * 0x40), 0x1F);
_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 < 0x40; i += 2)
for (int i = 0; i < _flash_buffer_size; i += 2)
{
__asm("nop");
data = data_buffer[(j * 0x40) + i + 1] << 8 | data_buffer[(j * 0x40) + i];
_FLASH_WRITE(address + (j * 0x40) + 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 * 0x40), 0x2A);
_FLASH_WRITE(address + (j * _flash_buffer_size), 0x2A);
while (1)
{
__asm("nop");
if (p_rom[(j * 0x20) + 0x1F] == data)
if (p_rom[(j * (_flash_buffer_size >> 1)) + ((_flash_buffer_size >> 1) - 1)] == data)
{
break;
}
@@ -337,9 +354,19 @@ 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
u32 timeout = 0x2FFF;

View File

@@ -29,6 +29,7 @@ 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;
@@ -118,19 +119,6 @@ int main(void) {
}
}
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)) {
sFlashStatus.magic = MAGIC_FLASH_STATUS;
sFlashStatus.version = 0;
sFlashStatus.battery_present = 1;
sFlashStatus.last_boot_menu_index = 0xFFFF;
sFlashStatus.last_boot_save_index = 0xFF;
sFlashStatus.last_boot_save_type = SRAM_NONE;
} else {
cursor_pos = sFlashStatus.last_boot_menu_index % 8;
page_active = sFlashStatus.last_boot_menu_index / 8;
}
// Count number of ROMs
for (roms_total = 0; roms_total < 512; roms_total++) {
memcpy(&sItemConfig, ((u8*)itemlist+itemlist_offset)+(0x70*roms_total), sizeof(sItemConfig));
@@ -154,6 +142,19 @@ int main(void) {
}
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)) {
sFlashStatus.magic = MAGIC_FLASH_STATUS;
sFlashStatus.version = 0;
sFlashStatus.battery_present = 1;
sFlashStatus.last_boot_menu_index = 0xFFFF;
sFlashStatus.last_boot_save_index = 0xFF;
sFlashStatus.last_boot_save_type = SRAM_NONE;
} else {
cursor_pos = sFlashStatus.last_boot_menu_index % 8;
page_active = sFlashStatus.last_boot_menu_index / 8;
}
s32 wait = 0;
u8 f = 0;
while (1) {
@@ -188,7 +189,7 @@ int main(void) {
}
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);
@@ -198,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) {
@@ -221,7 +222,7 @@ int main(void) {
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);