mirror of
https://github.com/lesserkuma/GBA_MultiMenu.git
synced 2026-08-23 09:04:28 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d51437552e | ||
|
|
12d953d8fd | ||
|
|
1bf4927d6b | ||
|
|
a3c4a44893 | ||
|
|
cbe17ef7c5 | ||
|
|
8dc1d10a95 |
2
Makefile
2
Makefile
@@ -183,7 +183,9 @@ $(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
|
||||
|
||||
17
README.md
17
README.md
@@ -15,15 +15,19 @@ The following section must be edited in order to specify the cartridge type to u
|
||||
```json
|
||||
"cartridge": {
|
||||
"type": 2,
|
||||
"battery_present": false
|
||||
"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)
|
||||
- `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)
|
||||
|
||||
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": [
|
||||
@@ -71,15 +75,18 @@ If the cartridge has a battery installed, the ROMs must be SRAM-patched with [GB
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Thanks
|
||||
Thanks to FraX, Ausar, liuyunx, BennVenn
|
||||
Thanks to FraX, Ausar, liuyunx, BennVenn, Jenetrix
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime
|
||||
|
||||
# Configuration
|
||||
app_version = "0.4"
|
||||
app_version = "0.9"
|
||||
default_file = "LK_MULTIMENU_<CODE>.gba"
|
||||
|
||||
################################
|
||||
@@ -51,6 +51,12 @@ cartridge_types = [
|
||||
"sector_size":0x40000,
|
||||
"block_size":0x80000,
|
||||
},
|
||||
{
|
||||
"name":"MSP54LV100",
|
||||
"flash_size":0x8000000,
|
||||
"sector_size":0x20000,
|
||||
"block_size":0x80000,
|
||||
},
|
||||
]
|
||||
now = datetime.datetime.now()
|
||||
log = ""
|
||||
@@ -83,6 +89,7 @@ if not os.path.exists(args.config):
|
||||
games = []
|
||||
cartridge_type = 1
|
||||
battery_present = False
|
||||
min_rom_size = 0x400000
|
||||
for file in files:
|
||||
d = {
|
||||
"enabled": True,
|
||||
@@ -102,6 +109,7 @@ if not os.path.exists(args.config):
|
||||
"cartridge": {
|
||||
"type": cartridge_type + 1,
|
||||
"battery_present": battery_present,
|
||||
"min_rom_size": min_rom_size,
|
||||
},
|
||||
"games": games,
|
||||
}
|
||||
@@ -124,6 +132,10 @@ else:
|
||||
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"]
|
||||
@@ -172,6 +184,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:
|
||||
@@ -316,7 +335,7 @@ logp("Menu ROM: 0x{:07X}–0x{:07X}".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("Status Area: 0x{:07X}–0x{:07X}".format(status_offset * sector_size, status_offset * sector_size + 0x1000))
|
||||
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 Code: {:s}".format(rom_code))
|
||||
output_file = output_file.replace("<CODE>", rom_code)
|
||||
@@ -326,7 +345,7 @@ if args.split:
|
||||
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])
|
||||
|
||||
@@ -66,6 +66,22 @@ IWRAM_CODE void FlashDetectType(void)
|
||||
return;
|
||||
}
|
||||
|
||||
// 1G cart with MSP54LV100S (Zelda Classic Collection 7-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)
|
||||
{
|
||||
REG_IE = ie;
|
||||
flash_type = 3;
|
||||
flash_sector_size = 0x20000;
|
||||
FlashCalcOffsets();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown type
|
||||
REG_IE = ie;
|
||||
flash_type = 0;
|
||||
@@ -119,6 +135,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;
|
||||
}
|
||||
@@ -191,6 +225,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 / 0x40); j++)
|
||||
{
|
||||
_FLASH_WRITE(0xAAA, 0xA9);
|
||||
_FLASH_WRITE(0x555, 0x56);
|
||||
_FLASH_WRITE(address + (j * 0x40), 0x26);
|
||||
_FLASH_WRITE(address + (j * 0x40), 0x1F);
|
||||
u16 data = 0;
|
||||
for (int i = 0; i < 0x40; i += 2)
|
||||
{
|
||||
__asm("nop");
|
||||
data = data_buffer[(j * 0x40) + i + 1] << 8 | data_buffer[(j * 0x40) + i];
|
||||
_FLASH_WRITE(address + (j * 0x40) + i, data);
|
||||
}
|
||||
_FLASH_WRITE(address + (j * 0x40), 0x2A);
|
||||
while (1)
|
||||
{
|
||||
__asm("nop");
|
||||
if (p_rom[(j * 0x20) + 0x1F] == data)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_FLASH_WRITE(address, 0xF0);
|
||||
}
|
||||
|
||||
REG_IE = ie;
|
||||
}
|
||||
@@ -215,14 +276,11 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
||||
if (_flash_type == 0)
|
||||
return 1;
|
||||
|
||||
// Temporarily store SRAM values 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[2] = *(vu8 *)MAPPER_CONFIG3;
|
||||
sram_register_backup[3] = *(vu8 *)MAPPER_CONFIG4;
|
||||
}
|
||||
// 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;
|
||||
sram_register_backup[3] = *(vu8 *)MAPPER_CONFIG4;
|
||||
|
||||
// Enable SRAM access
|
||||
*(vu8 *)MAPPER_CONFIG4 = 1;
|
||||
@@ -243,16 +301,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;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ Author: Lesserkuma (github.com/lesserkuma)
|
||||
}
|
||||
|
||||
#define MAGIC_FLASH_STATUS 0x414D554B
|
||||
#define FLASH_STATUS_LOCATION 0xA0000
|
||||
|
||||
typedef struct __attribute__((packed)) FlashStatus_
|
||||
{
|
||||
|
||||
@@ -233,13 +233,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;
|
||||
|
||||
Reference in New Issue
Block a user