mirror of
https://github.com/lesserkuma/GBA_MultiMenu.git
synced 2026-08-23 17:14:10 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d51437552e | ||
|
|
12d953d8fd | ||
|
|
1bf4927d6b | ||
|
|
a3c4a44893 | ||
|
|
cbe17ef7c5 | ||
|
|
8dc1d10a95 | ||
|
|
bcb9ae5d65 | ||
|
|
a33f0ed9db | ||
|
|
34b1b4b8dc | ||
|
|
e21ad43d20 | ||
|
|
8f63cdb03f | ||
|
|
9499327b3b | ||
|
|
2c88286a66 | ||
|
|
ce1212d365 | ||
|
|
8cd4c1b953 | ||
|
|
d8df1ff28e | ||
|
|
b43c23db22 |
BIN
.github/screen.png
vendored
Normal file
BIN
.github/screen.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
3
Makefile
3
Makefile
@@ -179,10 +179,13 @@ soundbank.bin soundbank.h : $(AUDIOFILES)
|
|||||||
|
|
||||||
#replacement rule for gbafix
|
#replacement rule for gbafix
|
||||||
#---------------------------------------------------------------------------------
|
#---------------------------------------------------------------------------------
|
||||||
|
$(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
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -13,18 +13,32 @@ 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:
|
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
|
```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 `1` or `2`:
|
||||||
- `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)
|
||||||
|
|
||||||
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
|
||||||
|
"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.
|
- `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.
|
- `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.
|
- `title` is the unicode title that will be displayed in the menu.
|
||||||
@@ -36,6 +50,7 @@ In the `games` section, you can edit the game-related stuff:
|
|||||||
- `5` = Nintendo DSi IPL font (KOR)
|
- `5` = Nintendo DSi IPL font (KOR)
|
||||||
- `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.
|
||||||
|
|
||||||
### ROM Builder Command Line Arguments
|
### ROM Builder Command Line Arguments
|
||||||
|
|
||||||
@@ -60,9 +75,19 @@ 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/).
|
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
|
||||||
|
|
||||||
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 [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, Jenetrix
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/lesserkuma/GBA_MultiMenu/master/.github/screen.png" alt="" />
|
||||||
|
|||||||
@@ -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.1"
|
app_version = "0.9"
|
||||||
default_file = "LK_MULTIMENU_<CODE>.gba"
|
default_file = "LK_MULTIMENU_<CODE>.gba"
|
||||||
|
|
||||||
################################
|
################################
|
||||||
@@ -39,16 +39,24 @@ def logp(*args, **kwargs):
|
|||||||
################################
|
################################
|
||||||
|
|
||||||
cartridge_types = [
|
cartridge_types = [
|
||||||
{ # "MSP55LV100S
|
{
|
||||||
|
"name":"MSP55LV100S",
|
||||||
"flash_size":0x4000000,
|
"flash_size":0x4000000,
|
||||||
"sector_size":0x20000,
|
"sector_size":0x20000,
|
||||||
"block_size":0x80000,
|
"block_size":0x80000,
|
||||||
},
|
},
|
||||||
{ # 6600M0U0BE
|
{
|
||||||
|
"name":"6600M0U0BE",
|
||||||
"flash_size":0x10000000,
|
"flash_size":0x10000000,
|
||||||
"sector_size":0x40000,
|
"sector_size":0x40000,
|
||||||
"block_size":0x80000,
|
"block_size":0x80000,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"MSP54LV100",
|
||||||
|
"flash_size":0x8000000,
|
||||||
|
"sector_size":0x20000,
|
||||||
|
"block_size":0x80000,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
log = ""
|
log = ""
|
||||||
@@ -64,11 +72,11 @@ parser.add_argument("--output", type=str, default=default_file, help="sets the f
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
output_file = args.output
|
output_file = args.output
|
||||||
if output_file == "lk_multimenu.gba":
|
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")
|
if not args.no_wait: input("\nPress ENTER to exit.\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.exists("lk_multimenu.gba"):
|
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")
|
if not args.no_wait: input("\nPress ENTER to exit.\n")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
@@ -81,30 +89,53 @@ 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,
|
||||||
}
|
}
|
||||||
with open(args.config, "w", encoding="UTF-8-SIG") as f:
|
if len(games) == 0:
|
||||||
f.write(json.dumps(obj=obj, indent=4, ensure_ascii=False))
|
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:
|
else:
|
||||||
with open(args.config, "r", encoding="UTF-8-SIG") as f:
|
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"]
|
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"]
|
||||||
@@ -128,15 +159,16 @@ UpdateSectorMap(start=0, length=math.ceil(len(menu_rom) / sector_size), c="m")
|
|||||||
item_list_offset = len(menu_rom)
|
item_list_offset = len(menu_rom)
|
||||||
item_list_offset = 0x40000 - (item_list_offset % 0x40000) + item_list_offset
|
item_list_offset = 0x40000 - (item_list_offset % 0x40000) + item_list_offset
|
||||||
item_list_offset = math.ceil(item_list_offset / sector_size)
|
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
|
status_offset = item_list_offset + 1
|
||||||
UpdateSectorMap(start=status_offset, length=1, c="c")
|
UpdateSectorMap(start=status_offset, length=1, c="c")
|
||||||
if battery_present:
|
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:
|
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
|
compilation[status_offset * sector_size:status_offset * sector_size + len(status)] = status
|
||||||
save_data_sector_offset = status_offset + 1
|
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
|
# Read game ROMs and import save data
|
||||||
saves_read = []
|
saves_read = []
|
||||||
@@ -152,6 +184,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:
|
||||||
@@ -190,7 +229,7 @@ else:
|
|||||||
|
|
||||||
games = [game for game in games if not ("missing" in game and game["missing"])]
|
games = [game for game in games if not ("missing" in game and game["missing"])]
|
||||||
if len(games) == 0:
|
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()
|
sys.exit()
|
||||||
|
|
||||||
# Add index
|
# Add index
|
||||||
@@ -201,19 +240,24 @@ 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)
|
||||||
boot_logo_found = False
|
|
||||||
c = 0
|
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)):
|
||||||
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"]:
|
if sector_map[i:i + game["sector_count"]] == ["."] * game["sector_count"]:
|
||||||
UpdateSectorMap(i, game["sector_count"], "r")
|
UpdateSectorMap(i, game["sector_count"], "r")
|
||||||
with open(f"roms/{game['file']}", "rb") as f: rom = f.read()
|
with open(f"roms/{game['file']}", "rb") as f: rom = f.read()
|
||||||
compilation[i * sector_size:i * sector_size + len(rom)] = rom
|
compilation[i * sector_size:i * sector_size + len(rom)] = rom
|
||||||
game["sector_offset"] = i
|
game["sector_offset"] = i
|
||||||
game["block_offset"] = game["sector_offset"] * sector_size // block_size
|
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
|
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 ]):
|
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,10 +284,10 @@ 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 | Size | Save Slot | Title")
|
logp (" | Offset | Map Size | Save Slot | Title")
|
||||||
toc_sep = "----+-----------+-----------+----------------+---------------------------------"
|
toc_sep = "----+-----------+-----------+----------------+---------------------------------"
|
||||||
else:
|
else:
|
||||||
logp (" | Offset | Size | Title")
|
logp (" | Offset | Map Size | Title")
|
||||||
toc_sep = "----+-----------+-----------+--------------------------------------------------"
|
toc_sep = "----+-----------+-----------+--------------------------------------------------"
|
||||||
|
|
||||||
item_list = bytearray()
|
item_list = bytearray()
|
||||||
@@ -259,12 +303,11 @@ for game in games:
|
|||||||
if game['save_type'] > 0:
|
if game['save_type'] > 0:
|
||||||
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
|
table_line += f"{game['save_slot']+1:2d} (0x{(save_data_sector_offset + game['save_slot']) * sector_size:07X}) | "
|
||||||
else:
|
else:
|
||||||
table_line += "-------------- | "
|
table_line += " | "
|
||||||
table_line += f"{title}"
|
table_line += f"{title}"
|
||||||
if c % 8 == 0: logp(toc_sep)
|
if c % 8 == 0: logp(toc_sep)
|
||||||
logp(table_line)
|
logp(table_line)
|
||||||
c += 1
|
c += 1
|
||||||
#logp(f"0x{game['block_offset'] * block_size:07X} |")
|
|
||||||
|
|
||||||
title = title.ljust(0x30, "\0")
|
title = title.ljust(0x30, "\0")
|
||||||
item_list += bytearray(struct.pack("B", game["title_font"]))
|
item_list += bytearray(struct.pack("B", game["title_font"]))
|
||||||
@@ -275,6 +318,7 @@ for game in games:
|
|||||||
item_list += bytearray(struct.pack("B", game["save_slot"]))
|
item_list += bytearray(struct.pack("B", game["save_slot"]))
|
||||||
item_list += bytearray([0] * 8)
|
item_list += bytearray([0] * 8)
|
||||||
item_list += bytearray(title.encode("UTF-16LE"))
|
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()
|
||||||
|
|
||||||
@@ -287,8 +331,13 @@ for i in range(0xA0, 0xBD):
|
|||||||
checksum = (checksum - 0x19) & 0xFF
|
checksum = (checksum - 0x19) & 0xFF
|
||||||
compilation[0xBD] = checksum
|
compilation[0xBD] = checksum
|
||||||
logp("")
|
logp("")
|
||||||
logp("Compilation ROM size: {:.2f} MiB".format(rom_size / 1024 / 1024))
|
logp("Menu ROM: 0x{:07X}–0x{:07X}".format(0, len(menu_rom)))
|
||||||
logp("ROM code: {:s}".format(rom_code))
|
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 + 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)
|
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)):
|
||||||
@@ -296,7 +345,7 @@ if args.split:
|
|||||||
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])
|
||||||
@@ -307,7 +356,3 @@ if not args.no_log:
|
|||||||
log += "\n################################\n\n"
|
log += "\n################################\n\n"
|
||||||
with open("log.txt", "ab") as f: f.write(log.encode("UTF-8-SIG"))
|
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")
|
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])
|
|
||||||
|
|||||||
106
source/flash.c
106
source/flash.c
@@ -66,6 +66,22 @@ IWRAM_CODE void FlashDetectType(void)
|
|||||||
return;
|
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
|
// Unknown type
|
||||||
REG_IE = ie;
|
REG_IE = ie;
|
||||||
flash_type = 0;
|
flash_type = 0;
|
||||||
@@ -119,6 +135,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;
|
||||||
}
|
}
|
||||||
@@ -191,6 +225,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 / 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;
|
REG_IE = ie;
|
||||||
}
|
}
|
||||||
@@ -215,7 +276,7 @@ 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
|
||||||
sram_register_backup[0] = *(vu8 *)MAPPER_CONFIG1;
|
sram_register_backup[0] = *(vu8 *)MAPPER_CONFIG1;
|
||||||
sram_register_backup[1] = *(vu8 *)MAPPER_CONFIG2;
|
sram_register_backup[1] = *(vu8 *)MAPPER_CONFIG2;
|
||||||
sram_register_backup[2] = *(vu8 *)MAPPER_CONFIG3;
|
sram_register_backup[2] = *(vu8 *)MAPPER_CONFIG3;
|
||||||
@@ -225,7 +286,8 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
*(vu8 *)MAPPER_CONFIG4 = 1;
|
*(vu8 *)MAPPER_CONFIG4 = 1;
|
||||||
|
|
||||||
// Write previous SRAM to flash
|
// Write previous SRAM to flash
|
||||||
if (status.battery_present) {
|
if (status.battery_present)
|
||||||
|
{
|
||||||
if (status.last_boot_save_type != SRAM_NONE)
|
if (status.last_boot_save_type != SRAM_NONE)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < SRAM_SIZE; i++)
|
for (int i = 0; i < SRAM_SIZE; i++)
|
||||||
@@ -239,16 +301,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;
|
||||||
|
|
||||||
@@ -279,7 +341,7 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
*(vu8 *)MAPPER_CONFIG3 = 0x40 - config.rom_size; // accessible ROM size (in 512 KB blocks)
|
*(vu8 *)MAPPER_CONFIG3 = 0x40 - config.rom_size; // accessible ROM size (in 512 KB blocks)
|
||||||
|
|
||||||
// Wait until menu ROM is no longer visible
|
// Wait until menu ROM is no longer visible
|
||||||
u16 timeout = 0xFFFF;
|
u32 timeout = 0x2FFF;
|
||||||
while (((vu16 *)AGB_ROM)[0x58] == 0x4B4C)
|
while (((vu16 *)AGB_ROM)[0x58] == 0x4B4C)
|
||||||
{
|
{
|
||||||
if (!timeout--)
|
if (!timeout--)
|
||||||
@@ -294,16 +356,20 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
// Lock mapper
|
// Lock mapper
|
||||||
*(vu8 *)MAPPER_CONFIG2 |= 0x80;
|
*(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
|
// 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
|
// Clear palette
|
||||||
@@ -313,7 +379,7 @@ IWRAM_CODE u8 BootGame(ItemConfig config, FlashStatus status)
|
|||||||
}
|
}
|
||||||
REG_BLDY = 0;
|
REG_BLDY = 0;
|
||||||
|
|
||||||
// Soft Reset system call
|
// Boot ROM
|
||||||
__asm("swi 0"); // Soft reset
|
__asm("swi 0"); // Soft reset
|
||||||
|
|
||||||
return 3;
|
return 3;
|
||||||
|
|||||||
@@ -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_
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Author: Lesserkuma (github.com/lesserkuma)
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "version.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "font.h"
|
#include "font.h"
|
||||||
#include "flash.h"
|
#include "flash.h"
|
||||||
@@ -106,7 +107,7 @@ int main(void) {
|
|||||||
page_total = (roms_total + 8.0 - 1) / 8.0;
|
page_total = (roms_total + 8.0 - 1) / 8.0;
|
||||||
|
|
||||||
memcpy(&sFlashStatus, (void *)(AGB_ROM + flash_status_sector_offset * flash_sector_size), sizeof(sFlashStatus));
|
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.magic = MAGIC_FLASH_STATUS;
|
||||||
sFlashStatus.version = 0;
|
sFlashStatus.version = 0;
|
||||||
sFlashStatus.battery_present = 1;
|
sFlashStatus.battery_present = 1;
|
||||||
@@ -186,14 +187,10 @@ int main(void) {
|
|||||||
}
|
}
|
||||||
} else if (show_credits) {
|
} else if (show_credits) {
|
||||||
LoadFont(0);
|
LoadFont(0);
|
||||||
#ifdef __TIMESTAMP_ISO__
|
memset(temp_unicode, 0, sizeof(temp_unicode));
|
||||||
memset(temp_unicode, 0, sizeof(temp_unicode));
|
snprintf(temp_ascii, 48, "Menu by LK - %s", BUILDTIME);
|
||||||
snprintf(temp_ascii, 48, "Menu by LK - %s", __TIMESTAMP_ISO__);
|
AsciiToUnicode(temp_ascii, temp_unicode);
|
||||||
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);
|
||||||
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
|
|
||||||
} else if (show_debug) {
|
} else if (show_debug) {
|
||||||
LoadFont(0);
|
LoadFont(0);
|
||||||
u8 a = ((sItemConfig.rom_offset / 0x40) & 0xF) << 4;
|
u8 a = ((sItemConfig.rom_offset / 0x40) & 0xF) << 4;
|
||||||
@@ -236,13 +233,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;
|
||||||
|
|||||||
15
source/version.h
Normal file
15
source/version.h
Normal 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
|
||||||
Reference in New Issue
Block a user