Add UTF-8 support and extended font

This commit is contained in:
spicyjpeg 2024-07-29 23:45:05 +02:00
parent 3f06ea5c50
commit 6ca0c02944
No known key found for this signature in database
GPG Key ID: 5CC87404C01DF393
31 changed files with 1034 additions and 543 deletions

View File

@ -121,6 +121,7 @@ addPS1Executable(
src/common/util/log.cpp src/common/util/log.cpp
src/common/util/misc.cpp src/common/util/misc.cpp
src/common/util/string.cpp src/common/util/string.cpp
src/common/util/string.s
src/common/util/tween.cpp src/common/util/tween.cpp
src/common/args.cpp src/common/args.cpp
src/common/gpu.cpp src/common/gpu.cpp
@ -219,6 +220,8 @@ addLauncher(803fd000 803ffff0)
## Boot stub and resource archive ## Boot stub and resource archive
file(GLOB_RECURSE assetList RELATIVE "${PROJECT_SOURCE_DIR}" assets/* )
configure_file(assets/about.txt about.txt NEWLINE_STYLE LF) configure_file(assets/about.txt about.txt NEWLINE_STYLE LF)
function(addBootStub name resourceName) function(addBootStub name resourceName)
@ -233,8 +236,7 @@ function(addBootStub name resourceName)
OUTPUT ${resourceName}.zip OUTPUT ${resourceName}.zip
DEPENDS DEPENDS
${resourceName}.json ${resourceName}.json
assets/app.palette.json ${assetList}
assets/app.strings.json
main.psexe main.psexe
launcher801fd000.psexe launcher801fd000.psexe
launcher803fd000.psexe launcher803fd000.psexe

View File

@ -1,7 +1,7 @@
{ {
"AboutScreen": { "AboutScreen": {
"title": "{RIGHT_ARROW} About 573in1", "title": " About 573in1",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back." "prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back."
}, },
"App": { "App": {
@ -73,7 +73,7 @@
"erase": "Erasing existing header...\nDo not turn off the 573.", "erase": "Erasing existing header...\nDo not turn off the 573.",
"write": "Writing new header...\nDo not turn off the 573.", "write": "Writing new header...\nDo not turn off the 573.",
"flashError": "An error occurred while erasing and rewriting the first sector of the internal flash memory.\n\nError code: %s\nPress the Test button to view debug logs.", "flashError": "An error occurred while erasing and rewriting the first sector of the internal flash memory.\n\nError code: %s\nPress the Test button to view debug logs.",
"unsupported": "The flash memory chips on this device are unresponsive to commands or are currently unsupported. If you are trying to erase a PCMCIA card with a write protect switch, make sure the switch is off.\n\nSee the documentation for more information on supported flash chips." "unsupported": "The internal flash memory chips are unresponsive to commands or are currently unsupported. This likely means the 573 motherboard is damaged.\n\nSee the documentation for more information on supported flash chips."
}, },
"qrCodeWorker": { "qrCodeWorker": {
"compress": "Compressing cartridge dump...", "compress": "Compressing cartridge dump...",
@ -122,9 +122,9 @@
}, },
"AudioTestScreen": { "AudioTestScreen": {
"title": "{RIGHT_ARROW} Audio output test", "title": " Audio output test",
"prompt": "Note that the speaker amplifier and analog audio passthrough are disabled by default.", "prompt": "Note that the speaker amplifier and analog audio passthrough are disabled by default.",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"playLeft": "Play sound on left channel", "playLeft": "Play sound on left channel",
"playRight": "Play sound on right channel", "playRight": "Play sound on right channel",
@ -140,13 +140,13 @@
"rom": "A valid boot executable has been found on the internal flash memory or a PCMCIA card and will be launched shortly. You may disable automatic booting globally by turning off DIP switch 1 or for flash devices only by using DIP switch 4.", "rom": "A valid boot executable has been found on the internal flash memory or a PCMCIA card and will be launched shortly. You may disable automatic booting globally by turning off DIP switch 1 or for flash devices only by using DIP switch 4.",
"ide": "A valid boot executable has been found on an IDE drive and will be launched shortly. You may disable automatic booting globally by turning off DIP switch 1 or per-drive by creating a file named noboot.txt in the root of the filesystem.\n\nFile: %s", "ide": "A valid boot executable has been found on an IDE drive and will be launched shortly. You may disable automatic booting globally by turning off DIP switch 1 or per-drive by creating a file named noboot.txt in the root of the filesystem.\n\nFile: %s",
"cancel": "{START_BUTTON} Cancel (%ds)" "cancel": " Cancel (%ds)"
}, },
"ButtonMappingScreen": { "ButtonMappingScreen": {
"title": "{RIGHT_ARROW} Select button mapping", "title": " Select button mapping",
"prompt": "Use {START_BUTTON} or the Test button to select a mapping preset suitable for your cabinet or JAMMA setup. Other buttons will be enabled once a mapping is selected.", "prompt": "Use or the Test button to select a mapping preset suitable for your cabinet or JAMMA setup. Other buttons will be enabled once a mapping is selected.",
"itemPrompt": "{RIGHT_ARROW} Press and hold {START_BUTTON} or Test to confirm", "itemPrompt": "▸ Press and hold ▭ or Test to confirm",
"joystick": "JAMMA supergun or joystick/buttons", "joystick": "JAMMA supergun or joystick/buttons",
"ddrCab": "Dance Dance Revolution (2-player) cabinet", "ddrCab": "Dance Dance Revolution (2-player) cabinet",
@ -166,8 +166,8 @@
}, },
"CartActionsScreen": { "CartActionsScreen": {
"title": "{RIGHT_ARROW} Cartridge options", "title": " Cartridge options",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"qrDump": { "qrDump": {
"name": "Dump cartridge as QR code", "name": "Dump cartridge as QR code",
@ -218,7 +218,7 @@
}, },
"CartInfoScreen": { "CartInfoScreen": {
"title": "{RIGHT_ARROW} Cartridge information", "title": " Cartridge information",
"digitalIO": { "digitalIO": {
"header": "Digital I/O board:\n", "header": "Digital I/O board:\n",
@ -233,8 +233,8 @@
"zs01Info": " Chip type:\t\tKonami ZS01 (PIC16CE625)\n Unlock status:\t%s\n DS2401 ID:\t\t%s\n ZS01 ID:\t\t%s\n Configuration:\t%s\n" "zs01Info": " Chip type:\t\tKonami ZS01 (PIC16CE625)\n Unlock status:\t%s\n DS2401 ID:\t\t%s\n ZS01 ID:\t\t%s\n Configuration:\t%s\n"
}, },
"unlockStatus": { "unlockStatus": {
"locked": "{CLOSED_LOCK} locked, game key required", "locked": "🔒 locked, game key required",
"unlocked": "{OPEN_LOCK} unlocked" "unlocked": "🔓 unlocked"
}, },
"id": { "id": {
"error": "read failure", "error": "read failure",
@ -265,9 +265,9 @@
} }
}, },
"prompt": { "prompt": {
"locked": "{RIGHT_ARROW} Press {START_BUTTON} to unlock, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.", "locked": "▸ Press ▭ to unlock, hold ◁▷ + ▭ to go back.",
"unlocked": "{RIGHT_ARROW} Press {START_BUTTON} to continue, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.", "unlocked": "▸ Press ▭ to continue, hold ◁▷ + ▭ to go back.",
"error": "{RIGHT_ARROW} Hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back." "error": "▸ Hold ◁▷ + ▭ to go back."
}, },
"unlockWarning": { "unlockWarning": {
@ -278,8 +278,8 @@
}, },
"ChecksumScreen": { "ChecksumScreen": {
"title": "{RIGHT_ARROW} Storage device checksums", "title": " Storage device checksums",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back.", "prompt": "▸ Press ▭ to go back.",
"bios": "BIOS ROM (512 KB):\t\t\t\t%08X\n", "bios": "BIOS ROM (512 KB):\t\t\t\t%08X\n",
"rtc": "RTC RAM (8184 bytes):\t\t\t%08X\n", "rtc": "RTC RAM (8184 bytes):\t\t\t%08X\n",
@ -289,8 +289,8 @@
}, },
"ColorIntensityScreen": { "ColorIntensityScreen": {
"title": "{RIGHT_ARROW} Monitor color intensity test", "title": " Monitor color intensity test",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back.", "prompt": "▸ Press ▭ to go back.",
"white": "White", "white": "White",
"red": "Red", "red": "Red",
@ -305,18 +305,18 @@
}, },
"FileBrowserScreen": { "FileBrowserScreen": {
"title": "{RIGHT_ARROW} Select file", "title": " Select file",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"parentDir": "[Parent directory]", "parentDir": "[Parent directory]",
"subdirError": "An error occurred while enumerating files in the selected subdirectory. The filesystem may be corrupted or otherwise inaccessible.\n\nPath: %s\nPress the Test button to view debug logs." "subdirError": "An error occurred while enumerating files in the selected subdirectory. The filesystem may be corrupted or otherwise inaccessible.\n\nPath: %s\nPress the Test button to view debug logs."
}, },
"FilePickerScreen": { "FilePickerScreen": {
"title": "{RIGHT_ARROW} Select IDE drive", "title": " Select IDE drive",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"host": "{HOST_ICON} Host filesystem (PCDRV)", "host": "🖧 Host filesystem (PCDRV)",
"noFS": "no disc or unsupported FS", "noFS": "no disc or unsupported FS",
"noDeviceError": "No drives have been found and successfully initialized on the IDE bus. Make sure the drives are appropriately configured as primary or secondary and are receiving power.\n\nPress the Test button to view debug logs.", "noDeviceError": "No drives have been found and successfully initialized on the IDE bus. Make sure the drives are appropriately configured as primary or secondary and are receiving power.\n\nPress the Test button to view debug logs.",
@ -326,18 +326,18 @@
}, },
"GeometryScreen": { "GeometryScreen": {
"title": "{RIGHT_ARROW} Monitor geometry test", "title": " Monitor geometry test",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back." "prompt": "▸ Press ▭ to go back."
}, },
"HexdumpScreen": { "HexdumpScreen": {
"title": "{RIGHT_ARROW} Cartridge dump", "title": " Cartridge dump",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back." "prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back."
}, },
"IDEInfoScreen": { "IDEInfoScreen": {
"title": "{RIGHT_ARROW} IDE device information", "title": " IDE device information",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back.", "prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back.",
"device": { "device": {
"header": { "header": {
@ -371,8 +371,8 @@
}, },
"JAMMATestScreen": { "JAMMATestScreen": {
"title": "{RIGHT_ARROW} JAMMA input test", "title": " JAMMA input test",
"prompt": "{RIGHT_ARROW} Press and hold {START_BUTTON} to go back.", "prompt": "▸ Press and hold ▭ to go back.",
"noInputs": "No button is currently held down. If a button does not appear here when pressed, make sure the JAMMA harness is wired up correctly and the button is not damaged.\n", "noInputs": "No button is currently held down. If a button does not appear here when pressed, make sure the JAMMA harness is wired up correctly and the button is not damaged.\n",
"inputs": "The following buttons are currently held down:\n", "inputs": "The following buttons are currently held down:\n",
@ -389,7 +389,7 @@
"button4": " Player 1 button 4\t\tJAMMA pin 25\n", "button4": " Player 1 button 4\t\tJAMMA pin 25\n",
"button5": " Player 1 button 5\t\tJAMMA pin 26\n", "button5": " Player 1 button 5\t\tJAMMA pin 26\n",
"button6": " Player 1 button 6\n", "button6": " Player 1 button 6\n",
"start": " Player 1 start button ({START_BUTTON})\tJAMMA pin 17\n" "start": " Player 1 start button ()\tJAMMA pin 17\n"
}, },
"p2": { "p2": {
"left": " Player 2 joystick left\t\tJAMMA pin X\n", "left": " Player 2 joystick left\t\tJAMMA pin X\n",
@ -402,7 +402,7 @@
"button4": " Player 2 button 4\t\tJAMMA pin c\n", "button4": " Player 2 button 4\t\tJAMMA pin c\n",
"button5": " Player 2 button 5\t\tJAMMA pin d\n", "button5": " Player 2 button 5\t\tJAMMA pin d\n",
"button6": " Player 2 button 6\n", "button6": " Player 2 button 6\n",
"start": " Player 2 start button ({START_BUTTON})\tJAMMA pin U\n" "start": " Player 2 start button ()\tJAMMA pin U\n"
}, },
"coin1": " Coin switch 1\t\t\tJAMMA pin 16\n", "coin1": " Coin switch 1\t\t\tJAMMA pin 16\n",
@ -413,14 +413,14 @@
"KeyEntryScreen": { "KeyEntryScreen": {
"title": "Enter unlocking key", "title": "Enter unlocking key",
"body": "Enter the 8-byte key this cartridge was last locked with.\n\nUse {LEFT_BUTTON}{RIGHT_BUTTON} to move the cursor, hold {START_BUTTON} and use {LEFT_BUTTON}{RIGHT_BUTTON} to edit the highlighted digit.", "body": "Enter the 8-byte key this cartridge was last locked with.\n\nUse ◁▷ to move the cursor, hold ▭ and use ◁▷ to edit the highlighted digit.",
"cancel": "Cancel", "cancel": "Cancel",
"ok": "Confirm" "ok": "Confirm"
}, },
"MainMenuScreen": { "MainMenuScreen": {
"title": "{RIGHT_ARROW} 573in1", "title": " 573in1",
"itemPrompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to move, select by pressing {START_BUTTON}", "itemPrompt": "▸ Use ◁▷ to move, select by pressing ▭",
"cartInfo": { "cartInfo": {
"name": "Manage security cartridge", "name": "Manage security cartridge",
@ -476,20 +476,20 @@
}, },
"QRCodeScreen": { "QRCodeScreen": {
"title": "{RIGHT_ARROW} Cartridge dump", "title": " Cartridge dump",
"prompt": "Scan this code and paste the resulting string into the decodeDump.py script provided alongside 573in1 to obtain a dump of the cartridge. Press {START_BUTTON} to go back." "prompt": "Scan this code and paste the resulting string into the decodeDump.py script provided alongside 573in1 to obtain a dump of the cartridge. Press to go back."
}, },
"ReflashGameScreen": { "ReflashGameScreen": {
"title": "{RIGHT_ARROW} Select game to convert cartridge to", "title": " Select game to convert cartridge to",
"prompt": "Make sure you select the correct region. Note that cartridges can only be converted for use with games that accept the same cartridge type.", "prompt": "Make sure you select the correct region. Note that cartridges can only be converted for use with games that accept the same cartridge type.",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back" "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back"
}, },
"ResolutionScreen": { "ResolutionScreen": {
"title": "{RIGHT_ARROW} Select screen resolution", "title": " Select screen resolution",
"prompt": "Select a resolution appropriate for your monitor or upscaler setup. Note that interlaced modes may be subject to flickering.", "prompt": "Select a resolution appropriate for your monitor or upscaler setup. Note that interlaced modes may be subject to flickering.",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"320x240p": "320x240 (4:3), progressive", "320x240p": "320x240 (4:3), progressive",
"320x240i": "320x240 (4:3), interlaced (line doubled)", "320x240i": "320x240 (4:3), interlaced (line doubled)",
@ -504,14 +504,14 @@
"RTCTimeScreen": { "RTCTimeScreen": {
"title": "Set RTC date and time", "title": "Set RTC date and time",
"body": "Enter the current date and time. Note that System 573 games only accept years in 1970-2069 range.\n\nUse {LEFT_BUTTON}{RIGHT_BUTTON} to move the cursor, hold {START_BUTTON} and use {LEFT_BUTTON}{RIGHT_BUTTON} to edit the highlighted field.", "body": "Enter the current date and time. Note that System 573 games only accept years in 1970-2069 range.\n\nUse ◁▷ to move the cursor, hold ▭ and use ◁▷ to edit the highlighted field.",
"cancel": "Cancel", "cancel": "Cancel",
"ok": "Confirm" "ok": "Confirm"
}, },
"StorageActionsScreen": { "StorageActionsScreen": {
"title": "{RIGHT_ARROW} Storage device options", "title": " Storage device options",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"cardError": "The selected PCMCIA slot is empty.\n\nTurn off the system and insert a supported PCMCIA linear flash card in order to continue. DO NOT HOTPLUG CARDS; hotplugging may damage both the 573 and the card.", "cardError": "The selected PCMCIA slot is empty.\n\nTurn off the system and insert a supported PCMCIA linear flash card in order to continue. DO NOT HOTPLUG CARDS; hotplugging may damage both the 573 and the card.",
"runExecutable": { "runExecutable": {
@ -616,8 +616,8 @@
}, },
"StorageInfoScreen": { "StorageInfoScreen": {
"title": "{RIGHT_ARROW} Storage device information", "title": " Storage device information",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} for more options, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.", "prompt": "▸ Press ▭ for more options, hold ◁▷ + ▭ to go back.",
"bios": { "bios": {
"header": "BIOS ROM:\n", "header": "BIOS ROM:\n",
@ -666,14 +666,14 @@
"SystemIDEntryScreen": { "SystemIDEntryScreen": {
"title": "Edit system identifier", "title": "Edit system identifier",
"body": "Enter the new digital I/O board's identifier. To obtain the ID of another board, run 573in1 on its respective system.\n\nUse {LEFT_BUTTON}{RIGHT_BUTTON} to move the cursor, hold {START_BUTTON} and use {LEFT_BUTTON}{RIGHT_BUTTON} to edit the highlighted digit.", "body": "Enter the new digital I/O board's identifier. To obtain the ID of another board, run 573in1 on its respective system.\n\nUse ◁▷ to move the cursor, hold ▭ and use ◁▷ to edit the highlighted digit.",
"cancel": "Cancel", "cancel": "Cancel",
"ok": "Confirm" "ok": "Confirm"
}, },
"TestMenuScreen": { "TestMenuScreen": {
"title": "{RIGHT_ARROW} Hardware test suite", "title": " Hardware test suite",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"jammaTest": { "jammaTest": {
"name": "Test JAMMA inputs", "name": "Test JAMMA inputs",
@ -694,9 +694,9 @@
}, },
"UnlockKeyScreen": { "UnlockKeyScreen": {
"title": "{RIGHT_ARROW} Select unlocking key", "title": " Select unlocking key",
"prompt": "If the cartridge has been converted before, select the game it was last converted to. If it is currently blank, select 00-00-00-00-00-00-00-00.", "prompt": "If the cartridge has been converted before, select the game it was last converted to. If it is currently blank, select 00-00-00-00-00-00-00-00.",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back", "itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"autoUnlock": "Use key from identified game (recommended)", "autoUnlock": "Use key from identified game (recommended)",
"useCustomKey": "Enter key manually...", "useCustomKey": "Enter key manually...",
@ -709,7 +709,7 @@
"body": "573in1 is an experimental tool provided with no warranty whatsoever. It is not guaranteed to work and improper usage may PERMANENTLY BRICK your System 573 security cartridges.\n\nUse this tool at your own risk. Do not proceed if you do not know what you are doing.", "body": "573in1 is an experimental tool provided with no warranty whatsoever. It is not guaranteed to work and improper usage may PERMANENTLY BRICK your System 573 security cartridges.\n\nUse this tool at your own risk. Do not proceed if you do not know what you are doing.",
"cooldown": "Wait... (%ds)", "cooldown": "Wait... (%ds)",
"ok": "{START_BUTTON} Continue" "ok": " Continue"
}, },
"WorkerStatusScreen": { "WorkerStatusScreen": {

341
assets/textures/font.json Normal file
View File

@ -0,0 +1,341 @@
{
"$schema": "../../schema/metrics.json",
"spaceWidth": 4,
"tabWidth": 32,
"lineHeight": 10,
"baselineOffset": -1,
"characterSizes": {
" ": { "x": 0, "y": 0, "width": 4, "height": 10 },
"!": { "x": 6, "y": 0, "width": 2, "height": 10 },
"\"": { "x": 12, "y": 0, "width": 4, "height": 10 },
"#": { "x": 18, "y": 0, "width": 6, "height": 10 },
"$": { "x": 24, "y": 0, "width": 6, "height": 10 },
"%": { "x": 30, "y": 0, "width": 6, "height": 10 },
"&": { "x": 36, "y": 0, "width": 6, "height": 10 },
"'": { "x": 42, "y": 0, "width": 2, "height": 10 },
"(": { "x": 48, "y": 0, "width": 3, "height": 10 },
")": { "x": 54, "y": 0, "width": 3, "height": 10 },
"*": { "x": 60, "y": 0, "width": 4, "height": 10 },
"+": { "x": 66, "y": 0, "width": 6, "height": 10 },
",": { "x": 72, "y": 0, "width": 3, "height": 10 },
"-": { "x": 78, "y": 0, "width": 6, "height": 10 },
".": { "x": 84, "y": 0, "width": 2, "height": 10 },
"/": { "x": 90, "y": 0, "width": 6, "height": 10 },
"0": { "x": 0, "y": 10, "width": 6, "height": 10 },
"1": { "x": 6, "y": 10, "width": 6, "height": 10 },
"2": { "x": 12, "y": 10, "width": 6, "height": 10 },
"3": { "x": 18, "y": 10, "width": 6, "height": 10 },
"4": { "x": 24, "y": 10, "width": 6, "height": 10 },
"5": { "x": 30, "y": 10, "width": 6, "height": 10 },
"6": { "x": 36, "y": 10, "width": 6, "height": 10 },
"7": { "x": 42, "y": 10, "width": 6, "height": 10 },
"8": { "x": 48, "y": 10, "width": 6, "height": 10 },
"9": { "x": 54, "y": 10, "width": 6, "height": 10 },
":": { "x": 60, "y": 10, "width": 2, "height": 10 },
";": { "x": 66, "y": 10, "width": 3, "height": 10 },
"<": { "x": 72, "y": 10, "width": 6, "height": 10 },
"=": { "x": 78, "y": 10, "width": 6, "height": 10 },
">": { "x": 84, "y": 10, "width": 6, "height": 10 },
"?": { "x": 90, "y": 10, "width": 6, "height": 10 },
"@": { "x": 0, "y": 20, "width": 6, "height": 10 },
"A": { "x": 6, "y": 20, "width": 6, "height": 10 },
"B": { "x": 12, "y": 20, "width": 6, "height": 10 },
"C": { "x": 18, "y": 20, "width": 6, "height": 10 },
"D": { "x": 24, "y": 20, "width": 6, "height": 10 },
"E": { "x": 30, "y": 20, "width": 6, "height": 10 },
"F": { "x": 36, "y": 20, "width": 6, "height": 10 },
"G": { "x": 42, "y": 20, "width": 6, "height": 10 },
"H": { "x": 48, "y": 20, "width": 6, "height": 10 },
"I": { "x": 54, "y": 20, "width": 4, "height": 10 },
"J": { "x": 60, "y": 20, "width": 5, "height": 10 },
"K": { "x": 66, "y": 20, "width": 6, "height": 10 },
"L": { "x": 72, "y": 20, "width": 6, "height": 10 },
"M": { "x": 78, "y": 20, "width": 6, "height": 10 },
"N": { "x": 84, "y": 20, "width": 6, "height": 10 },
"O": { "x": 90, "y": 20, "width": 6, "height": 10 },
"P": { "x": 0, "y": 30, "width": 6, "height": 10 },
"Q": { "x": 6, "y": 30, "width": 6, "height": 10 },
"R": { "x": 12, "y": 30, "width": 6, "height": 10 },
"S": { "x": 18, "y": 30, "width": 6, "height": 10 },
"T": { "x": 24, "y": 30, "width": 6, "height": 10 },
"U": { "x": 30, "y": 30, "width": 6, "height": 10 },
"V": { "x": 36, "y": 30, "width": 6, "height": 10 },
"W": { "x": 42, "y": 30, "width": 6, "height": 10 },
"X": { "x": 48, "y": 30, "width": 6, "height": 10 },
"Y": { "x": 54, "y": 30, "width": 6, "height": 10 },
"Z": { "x": 60, "y": 30, "width": 6, "height": 10 },
"[": { "x": 66, "y": 30, "width": 3, "height": 10 },
"\\": { "x": 72, "y": 30, "width": 6, "height": 10 },
"]": { "x": 78, "y": 30, "width": 3, "height": 10 },
"^": { "x": 84, "y": 30, "width": 4, "height": 10 },
"_": { "x": 90, "y": 30, "width": 6, "height": 10 },
"`": { "x": 0, "y": 40, "width": 3, "height": 10 },
"a": { "x": 6, "y": 40, "width": 6, "height": 10 },
"b": { "x": 12, "y": 40, "width": 6, "height": 10 },
"c": { "x": 18, "y": 40, "width": 6, "height": 10 },
"d": { "x": 24, "y": 40, "width": 6, "height": 10 },
"e": { "x": 30, "y": 40, "width": 6, "height": 10 },
"f": { "x": 36, "y": 40, "width": 5, "height": 10 },
"g": { "x": 42, "y": 40, "width": 6, "height": 10 },
"h": { "x": 48, "y": 40, "width": 5, "height": 10 },
"i": { "x": 54, "y": 40, "width": 2, "height": 10 },
"j": { "x": 60, "y": 40, "width": 4, "height": 10 },
"k": { "x": 66, "y": 40, "width": 5, "height": 10 },
"l": { "x": 72, "y": 40, "width": 2, "height": 10 },
"m": { "x": 78, "y": 40, "width": 6, "height": 10 },
"n": { "x": 84, "y": 40, "width": 5, "height": 10 },
"o": { "x": 90, "y": 40, "width": 6, "height": 10 },
"p": { "x": 0, "y": 50, "width": 6, "height": 10 },
"q": { "x": 6, "y": 50, "width": 6, "height": 10 },
"r": { "x": 12, "y": 50, "width": 6, "height": 10 },
"s": { "x": 18, "y": 50, "width": 6, "height": 10 },
"t": { "x": 24, "y": 50, "width": 5, "height": 10 },
"u": { "x": 30, "y": 50, "width": 5, "height": 10 },
"v": { "x": 36, "y": 50, "width": 6, "height": 10 },
"w": { "x": 42, "y": 50, "width": 6, "height": 10 },
"x": { "x": 48, "y": 50, "width": 6, "height": 10 },
"y": { "x": 54, "y": 50, "width": 6, "height": 10 },
"z": { "x": 60, "y": 50, "width": 5, "height": 10 },
"{": { "x": 66, "y": 50, "width": 4, "height": 10 },
"|": { "x": 72, "y": 50, "width": 2, "height": 10 },
"}": { "x": 78, "y": 50, "width": 4, "height": 10 },
"~": { "x": 84, "y": 50, "width": 6, "height": 10 },
"\u00a0": { "x": 0, "y": 60, "width": 4, "height": 10 },
"¡": { "x": 6, "y": 60, "width": 2, "height": 10 },
"¢": { "x": 12, "y": 60, "width": 5, "height": 10 },
"£": { "x": 18, "y": 60, "width": 6, "height": 10 },
"¤": { "x": 24, "y": 60, "width": 6, "height": 10 },
"¥": { "x": 30, "y": 60, "width": 6, "height": 10 },
"¦": { "x": 36, "y": 60, "width": 2, "height": 10 },
"§": { "x": 42, "y": 60, "width": 5, "height": 10 },
"¨": { "x": 48, "y": 60, "width": 4, "height": 10 },
"©": { "x": 54, "y": 60, "width": 6, "height": 10 },
"ª": { "x": 60, "y": 60, "width": 4, "height": 10 },
"«": { "x": 66, "y": 60, "width": 6, "height": 10 },
"¬": { "x": 72, "y": 60, "width": 6, "height": 10 },
"\u00ad": { "x": 78, "y": 60, "width": 4, "height": 10 },
"®": { "x": 84, "y": 60, "width": 6, "height": 10 },
"¯": { "x": 90, "y": 60, "width": 6, "height": 10 },
"°": { "x": 0, "y": 70, "width": 4, "height": 10 },
"±": { "x": 6, "y": 70, "width": 6, "height": 10 },
"²": { "x": 12, "y": 70, "width": 4, "height": 10 },
"³": { "x": 18, "y": 70, "width": 4, "height": 10 },
"´": { "x": 24, "y": 70, "width": 3, "height": 10 },
"µ": { "x": 30, "y": 70, "width": 5, "height": 10 },
"¶": { "x": 36, "y": 70, "width": 6, "height": 10 },
"·": { "x": 42, "y": 70, "width": 4, "height": 10 },
"¸": { "x": 48, "y": 70, "width": 5, "height": 10 },
"¹": { "x": 54, "y": 70, "width": 4, "height": 10 },
"º": { "x": 60, "y": 70, "width": 4, "height": 10 },
"»": { "x": 66, "y": 70, "width": 6, "height": 10 },
"¼": { "x": 72, "y": 70, "width": 6, "height": 10 },
"½": { "x": 78, "y": 70, "width": 6, "height": 10 },
"¾": { "x": 84, "y": 70, "width": 6, "height": 10 },
"¿": { "x": 90, "y": 70, "width": 6, "height": 10 },
"À": { "x": 0, "y": 80, "width": 6, "height": 10 },
"Á": { "x": 6, "y": 80, "width": 6, "height": 10 },
"Â": { "x": 12, "y": 80, "width": 6, "height": 10 },
"Ã": { "x": 18, "y": 80, "width": 6, "height": 10 },
"Ä": { "x": 24, "y": 80, "width": 6, "height": 10 },
"Å": { "x": 30, "y": 80, "width": 6, "height": 10 },
"Æ": { "x": 36, "y": 80, "width": 6, "height": 10 },
"Ç": { "x": 42, "y": 80, "width": 6, "height": 10 },
"È": { "x": 48, "y": 80, "width": 6, "height": 10 },
"É": { "x": 54, "y": 80, "width": 6, "height": 10 },
"Ê": { "x": 60, "y": 80, "width": 6, "height": 10 },
"Ë": { "x": 66, "y": 80, "width": 6, "height": 10 },
"Ì": { "x": 72, "y": 80, "width": 4, "height": 10 },
"Í": { "x": 78, "y": 80, "width": 4, "height": 10 },
"Î": { "x": 84, "y": 80, "width": 4, "height": 10 },
"Ï": { "x": 90, "y": 80, "width": 4, "height": 10 },
"Ð": { "x": 0, "y": 90, "width": 6, "height": 10 },
"Ñ": { "x": 6, "y": 90, "width": 6, "height": 10 },
"Ò": { "x": 12, "y": 90, "width": 6, "height": 10 },
"Ó": { "x": 18, "y": 90, "width": 6, "height": 10 },
"Ô": { "x": 24, "y": 90, "width": 6, "height": 10 },
"Õ": { "x": 30, "y": 90, "width": 6, "height": 10 },
"Ö": { "x": 36, "y": 90, "width": 6, "height": 10 },
"×": { "x": 42, "y": 90, "width": 6, "height": 10 },
"Ø": { "x": 48, "y": 90, "width": 6, "height": 10 },
"Ù": { "x": 54, "y": 90, "width": 6, "height": 10 },
"Ú": { "x": 60, "y": 90, "width": 6, "height": 10 },
"Û": { "x": 66, "y": 90, "width": 6, "height": 10 },
"Ü": { "x": 72, "y": 90, "width": 6, "height": 10 },
"Ý": { "x": 78, "y": 90, "width": 6, "height": 10 },
"Þ": { "x": 84, "y": 90, "width": 6, "height": 10 },
"ß": { "x": 90, "y": 90, "width": 6, "height": 10 },
"à": { "x": 0, "y": 100, "width": 6, "height": 10 },
"á": { "x": 6, "y": 100, "width": 6, "height": 10 },
"â": { "x": 12, "y": 100, "width": 6, "height": 10 },
"ã": { "x": 18, "y": 100, "width": 6, "height": 10 },
"ä": { "x": 24, "y": 100, "width": 6, "height": 10 },
"å": { "x": 30, "y": 100, "width": 6, "height": 10 },
"æ": { "x": 36, "y": 100, "width": 6, "height": 10 },
"ç": { "x": 42, "y": 100, "width": 6, "height": 10 },
"è": { "x": 48, "y": 100, "width": 6, "height": 10 },
"é": { "x": 54, "y": 100, "width": 6, "height": 10 },
"ê": { "x": 60, "y": 100, "width": 6, "height": 10 },
"ë": { "x": 66, "y": 100, "width": 6, "height": 10 },
"ì": { "x": 72, "y": 100, "width": 3, "height": 10 },
"í": { "x": 78, "y": 100, "width": 3, "height": 10 },
"î": { "x": 84, "y": 100, "width": 4, "height": 10 },
"ï": { "x": 90, "y": 100, "width": 4, "height": 10 },
"ð": { "x": 0, "y": 110, "width": 5, "height": 10 },
"ñ": { "x": 6, "y": 110, "width": 5, "height": 10 },
"ò": { "x": 12, "y": 110, "width": 6, "height": 10 },
"ó": { "x": 18, "y": 110, "width": 6, "height": 10 },
"ô": { "x": 24, "y": 110, "width": 6, "height": 10 },
"õ": { "x": 30, "y": 110, "width": 6, "height": 10 },
"ö": { "x": 36, "y": 110, "width": 6, "height": 10 },
"÷": { "x": 42, "y": 110, "width": 6, "height": 10 },
"ø": { "x": 48, "y": 110, "width": 6, "height": 10 },
"ù": { "x": 54, "y": 110, "width": 5, "height": 10 },
"ú": { "x": 60, "y": 110, "width": 5, "height": 10 },
"û": { "x": 66, "y": 110, "width": 5, "height": 10 },
"ü": { "x": 72, "y": 110, "width": 5, "height": 10 },
"ý": { "x": 78, "y": 110, "width": 6, "height": 10 },
"þ": { "x": 84, "y": 110, "width": 5, "height": 10 },
"ÿ": { "x": 90, "y": 110, "width": 6, "height": 10 },
"▴": { "x": 0, "y": 120, "width": 6, "height": 10 },
"▾": { "x": 6, "y": 120, "width": 6, "height": 10 },
"◂": { "x": 12, "y": 120, "width": 4, "height": 10 },
"▸": { "x": 18, "y": 120, "width": 4, "height": 10 },
"↑": { "x": 24, "y": 120, "width": 6, "height": 10 },
"↓": { "x": 30, "y": 120, "width": 6, "height": 10 },
"←": { "x": 36, "y": 120, "width": 6, "height": 10 },
"→": { "x": 42, "y": 120, "width": 6, "height": 10 },
"<22>": { "x": 48, "y": 120, "width": 6, "height": 10 },
"": { "x": 96, "y": 0, "width": 7, "height": 10 },
"ァ": { "x": 104, "y": 0, "width": 6, "height": 10 },
"ア": { "x": 112, "y": 0, "width": 7, "height": 10 },
"ィ": { "x": 120, "y": 0, "width": 5, "height": 10 },
"イ": { "x": 128, "y": 0, "width": 6, "height": 10 },
"ゥ": { "x": 136, "y": 0, "width": 6, "height": 10 },
"ウ": { "x": 144, "y": 0, "width": 6, "height": 10 },
"ェ": { "x": 152, "y": 0, "width": 6, "height": 10 },
"エ": { "x": 160, "y": 0, "width": 6, "height": 10 },
"ォ": { "x": 168, "y": 0, "width": 6, "height": 10 },
"オ": { "x": 176, "y": 0, "width": 7, "height": 10 },
"カ": { "x": 184, "y": 0, "width": 7, "height": 10 },
"ガ": { "x": 192, "y": 0, "width": 8, "height": 10 },
"キ": { "x": 200, "y": 0, "width": 6, "height": 10 },
"ギ": { "x": 208, "y": 0, "width": 8, "height": 10 },
"ク": { "x": 216, "y": 0, "width": 5, "height": 10 },
"グ": { "x": 96, "y": 10, "width": 8, "height": 10 },
"ケ": { "x": 104, "y": 10, "width": 7, "height": 10 },
"ゲ": { "x": 112, "y": 10, "width": 8, "height": 10 },
"コ": { "x": 120, "y": 10, "width": 5, "height": 10 },
"ゴ": { "x": 128, "y": 10, "width": 7, "height": 10 },
"サ": { "x": 136, "y": 10, "width": 6, "height": 10 },
"ザ": { "x": 144, "y": 10, "width": 8, "height": 10 },
"シ": { "x": 152, "y": 10, "width": 7, "height": 10 },
"ジ": { "x": 160, "y": 10, "width": 8, "height": 10 },
"ス": { "x": 168, "y": 10, "width": 6, "height": 10 },
"ズ": { "x": 176, "y": 10, "width": 8, "height": 10 },
"セ": { "x": 184, "y": 10, "width": 7, "height": 10 },
"ゼ": { "x": 192, "y": 10, "width": 8, "height": 10 },
"ソ": { "x": 200, "y": 10, "width": 5, "height": 10 },
"ゾ": { "x": 208, "y": 10, "width": 8, "height": 10 },
"タ": { "x": 216, "y": 10, "width": 5, "height": 10 },
"ダ": { "x": 96, "y": 20, "width": 8, "height": 10 },
"チ": { "x": 104, "y": 20, "width": 6, "height": 10 },
"ヂ": { "x": 112, "y": 20, "width": 8, "height": 10 },
"ッ": { "x": 120, "y": 20, "width": 7, "height": 10 },
"ツ": { "x": 128, "y": 20, "width": 7, "height": 10 },
"ヅ": { "x": 136, "y": 20, "width": 8, "height": 10 },
"テ": { "x": 144, "y": 20, "width": 6, "height": 10 },
"デ": { "x": 152, "y": 20, "width": 8, "height": 10 },
"ト": { "x": 160, "y": 20, "width": 4, "height": 10 },
"ド": { "x": 168, "y": 20, "width": 7, "height": 10 },
"ナ": { "x": 176, "y": 20, "width": 6, "height": 10 },
"ニ": { "x": 184, "y": 20, "width": 6, "height": 10 },
"ヌ": { "x": 192, "y": 20, "width": 5, "height": 10 },
"ネ": { "x": 200, "y": 20, "width": 6, "height": 10 },
"": { "x": 208, "y": 20, "width": 5, "height": 10 },
"ハ": { "x": 216, "y": 20, "width": 6, "height": 10 },
"バ": { "x": 96, "y": 30, "width": 8, "height": 10 },
"パ": { "x": 104, "y": 30, "width": 8, "height": 10 },
"ヒ": { "x": 112, "y": 30, "width": 5, "height": 10 },
"ビ": { "x": 120, "y": 30, "width": 7, "height": 10 },
"ピ": { "x": 128, "y": 30, "width": 8, "height": 10 },
"フ": { "x": 136, "y": 30, "width": 6, "height": 10 },
"ブ": { "x": 144, "y": 30, "width": 8, "height": 10 },
"プ": { "x": 152, "y": 30, "width": 8, "height": 10 },
"ヘ": { "x": 160, "y": 30, "width": 7, "height": 10 },
"ベ": { "x": 168, "y": 30, "width": 8, "height": 10 },
"ペ": { "x": 176, "y": 30, "width": 8, "height": 10 },
"ホ": { "x": 184, "y": 30, "width": 8, "height": 10 },
"ボ": { "x": 192, "y": 30, "width": 8, "height": 10 },
"ポ": { "x": 200, "y": 30, "width": 8, "height": 10 },
"マ": { "x": 208, "y": 30, "width": 7, "height": 10 },
"ミ": { "x": 216, "y": 30, "width": 4, "height": 10 },
"ム": { "x": 96, "y": 40, "width": 7, "height": 10 },
"メ": { "x": 104, "y": 40, "width": 5, "height": 10 },
"モ": { "x": 112, "y": 40, "width": 6, "height": 10 },
"ャ": { "x": 120, "y": 40, "width": 7, "height": 10 },
"ヤ": { "x": 128, "y": 40, "width": 7, "height": 10 },
"ュ": { "x": 136, "y": 40, "width": 6, "height": 10 },
"ユ": { "x": 144, "y": 40, "width": 7, "height": 10 },
"ョ": { "x": 152, "y": 40, "width": 5, "height": 10 },
"ヨ": { "x": 160, "y": 40, "width": 7, "height": 10 },
"ラ": { "x": 168, "y": 40, "width": 6, "height": 10 },
"リ": { "x": 176, "y": 40, "width": 5, "height": 10 },
"ル": { "x": 184, "y": 40, "width": 8, "height": 10 },
"レ": { "x": 192, "y": 40, "width": 6, "height": 10 },
"ロ": { "x": 200, "y": 40, "width": 5, "height": 10 },
"ヮ": { "x": 208, "y": 40, "width": 6, "height": 10 },
"ワ": { "x": 216, "y": 40, "width": 6, "height": 10 },
"ヰ": { "x": 96, "y": 50, "width": 7, "height": 10 },
"ヱ": { "x": 104, "y": 50, "width": 6, "height": 10 },
"ヲ": { "x": 112, "y": 50, "width": 6, "height": 10 },
"ン": { "x": 120, "y": 50, "width": 7, "height": 10 },
"ヴ": { "x": 128, "y": 50, "width": 8, "height": 10 },
"ヵ": { "x": 136, "y": 50, "width": 6, "height": 10 },
"ヶ": { "x": 144, "y": 50, "width": 6, "height": 10 },
"ヷ": { "x": 152, "y": 50, "width": 8, "height": 10 },
"ヸ": { "x": 160, "y": 50, "width": 8, "height": 10 },
"ヹ": { "x": 168, "y": 50, "width": 8, "height": 10 },
"ヺ": { "x": 176, "y": 50, "width": 8, "height": 10 },
"・": { "x": 184, "y": 50, "width": 4, "height": 10 },
"ー": { "x": 192, "y": 50, "width": 7, "height": 10 },
"ヽ": { "x": 200, "y": 50, "width": 5, "height": 10 },
"ヾ": { "x": 208, "y": 50, "width": 7, "height": 10 },
"ヿ": { "x": 216, "y": 50, "width": 6, "height": 10 },
"\u3000": { "x": 96, "y": 120, "width": 4, "height": 10 },
"、": { "x": 104, "y": 120, "width": 3, "height": 10 },
"。": { "x": 112, "y": 120, "width": 4, "height": 10 },
"〃": { "x": 120, "y": 120, "width": 5, "height": 10 },
"〈": { "x": 128, "y": 120, "width": 4, "height": 10 },
"〉": { "x": 136, "y": 120, "width": 4, "height": 10 },
"《": { "x": 144, "y": 120, "width": 6, "height": 10 },
"》": { "x": 152, "y": 120, "width": 6, "height": 10 },
"「": { "x": 160, "y": 120, "width": 3, "height": 10 },
"」": { "x": 168, "y": 120, "width": 3, "height": 10 },
"【": { "x": 176, "y": 120, "width": 4, "height": 10 },
"】": { "x": 184, "y": 120, "width": 4, "height": 10 },
"": { "x": 192, "y": 120, "width": 4, "height": 10 },
"": { "x": 200, "y": 120, "width": 4, "height": 10 },
"○": { "x": 224, "y": 0, "width": 10, "height": 10, "icon": true },
"✕": { "x": 234, "y": 0, "width": 10, "height": 10, "icon": true },
"△": { "x": 244, "y": 0, "width": 10, "height": 10, "icon": true },
"□": { "x": 224, "y": 10, "width": 10, "height": 10, "icon": true },
"◁": { "x": 234, "y": 10, "width": 7, "height": 10, "icon": true },
"▷": { "x": 244, "y": 10, "width": 7, "height": 10, "icon": true },
"▭": { "x": 224, "y": 20, "width": 9, "height": 10, "icon": true },
"🔒": { "x": 234, "y": 20, "width": 8, "height": 10, "icon": true },
"🔓": { "x": 244, "y": 20, "width": 10, "height": 10, "icon": true },
"🖸": { "x": 224, "y": 30, "width": 10, "height": 10, "icon": true },
"🖴": { "x": 234, "y": 30, "width": 10, "height": 10, "icon": true },
"🖧": { "x": 244, "y": 30, "width": 10, "height": 10, "icon": true },
"🗀": { "x": 224, "y": 40, "width": 10, "height": 10, "icon": true },
"🖿": { "x": 234, "y": 40, "width": 10, "height": 10, "icon": true },
"🗎": { "x": 244, "y": 40, "width": 10, "height": 10, "icon": true }
}
}

View File

@ -1,131 +0,0 @@
{
"$schema": "../../schema/metrics.json",
"spaceWidth": 4,
"tabWidth": 32,
"lineHeight": 10,
"characterSizes": {
" ": { "x": 0, "y": 0, "width": 4, "height": 9 },
"!": { "x": 6, "y": 0, "width": 2, "height": 9 },
"\"": { "x": 12, "y": 0, "width": 4, "height": 9 },
"#": { "x": 18, "y": 0, "width": 6, "height": 9 },
"$": { "x": 24, "y": 0, "width": 6, "height": 9 },
"%": { "x": 30, "y": 0, "width": 6, "height": 9 },
"&": { "x": 36, "y": 0, "width": 6, "height": 9 },
"'": { "x": 42, "y": 0, "width": 2, "height": 9 },
"(": { "x": 48, "y": 0, "width": 3, "height": 9 },
")": { "x": 54, "y": 0, "width": 3, "height": 9 },
"*": { "x": 60, "y": 0, "width": 4, "height": 9 },
"+": { "x": 66, "y": 0, "width": 6, "height": 9 },
",": { "x": 72, "y": 0, "width": 3, "height": 9 },
"-": { "x": 78, "y": 0, "width": 6, "height": 9 },
".": { "x": 84, "y": 0, "width": 2, "height": 9 },
"/": { "x": 90, "y": 0, "width": 6, "height": 9 },
"0": { "x": 0, "y": 9, "width": 6, "height": 9 },
"1": { "x": 6, "y": 9, "width": 6, "height": 9 },
"2": { "x": 12, "y": 9, "width": 6, "height": 9 },
"3": { "x": 18, "y": 9, "width": 6, "height": 9 },
"4": { "x": 24, "y": 9, "width": 6, "height": 9 },
"5": { "x": 30, "y": 9, "width": 6, "height": 9 },
"6": { "x": 36, "y": 9, "width": 6, "height": 9 },
"7": { "x": 42, "y": 9, "width": 6, "height": 9 },
"8": { "x": 48, "y": 9, "width": 6, "height": 9 },
"9": { "x": 54, "y": 9, "width": 6, "height": 9 },
":": { "x": 60, "y": 9, "width": 2, "height": 9 },
";": { "x": 66, "y": 9, "width": 3, "height": 9 },
"<": { "x": 72, "y": 9, "width": 6, "height": 9 },
"=": { "x": 78, "y": 9, "width": 6, "height": 9 },
">": { "x": 84, "y": 9, "width": 6, "height": 9 },
"?": { "x": 90, "y": 9, "width": 6, "height": 9 },
"@": { "x": 0, "y": 18, "width": 6, "height": 9 },
"A": { "x": 6, "y": 18, "width": 6, "height": 9 },
"B": { "x": 12, "y": 18, "width": 6, "height": 9 },
"C": { "x": 18, "y": 18, "width": 6, "height": 9 },
"D": { "x": 24, "y": 18, "width": 6, "height": 9 },
"E": { "x": 30, "y": 18, "width": 6, "height": 9 },
"F": { "x": 36, "y": 18, "width": 6, "height": 9 },
"G": { "x": 42, "y": 18, "width": 6, "height": 9 },
"H": { "x": 48, "y": 18, "width": 6, "height": 9 },
"I": { "x": 54, "y": 18, "width": 4, "height": 9 },
"J": { "x": 60, "y": 18, "width": 5, "height": 9 },
"K": { "x": 66, "y": 18, "width": 6, "height": 9 },
"L": { "x": 72, "y": 18, "width": 6, "height": 9 },
"M": { "x": 78, "y": 18, "width": 6, "height": 9 },
"N": { "x": 84, "y": 18, "width": 6, "height": 9 },
"O": { "x": 90, "y": 18, "width": 6, "height": 9 },
"P": { "x": 0, "y": 27, "width": 6, "height": 9 },
"Q": { "x": 6, "y": 27, "width": 6, "height": 9 },
"R": { "x": 12, "y": 27, "width": 6, "height": 9 },
"S": { "x": 18, "y": 27, "width": 6, "height": 9 },
"T": { "x": 24, "y": 27, "width": 6, "height": 9 },
"U": { "x": 30, "y": 27, "width": 6, "height": 9 },
"V": { "x": 36, "y": 27, "width": 6, "height": 9 },
"W": { "x": 42, "y": 27, "width": 6, "height": 9 },
"X": { "x": 48, "y": 27, "width": 6, "height": 9 },
"Y": { "x": 54, "y": 27, "width": 6, "height": 9 },
"Z": { "x": 60, "y": 27, "width": 6, "height": 9 },
"[": { "x": 66, "y": 27, "width": 3, "height": 9 },
"\\": { "x": 72, "y": 27, "width": 6, "height": 9 },
"]": { "x": 78, "y": 27, "width": 3, "height": 9 },
"^": { "x": 84, "y": 27, "width": 4, "height": 9 },
"_": { "x": 90, "y": 27, "width": 6, "height": 9 },
"`": { "x": 0, "y": 36, "width": 3, "height": 9 },
"a": { "x": 6, "y": 36, "width": 6, "height": 9 },
"b": { "x": 12, "y": 36, "width": 6, "height": 9 },
"c": { "x": 18, "y": 36, "width": 6, "height": 9 },
"d": { "x": 24, "y": 36, "width": 6, "height": 9 },
"e": { "x": 30, "y": 36, "width": 6, "height": 9 },
"f": { "x": 36, "y": 36, "width": 5, "height": 9 },
"g": { "x": 42, "y": 36, "width": 6, "height": 9 },
"h": { "x": 48, "y": 36, "width": 5, "height": 9 },
"i": { "x": 54, "y": 36, "width": 2, "height": 9 },
"j": { "x": 60, "y": 36, "width": 4, "height": 9 },
"k": { "x": 66, "y": 36, "width": 5, "height": 9 },
"l": { "x": 72, "y": 36, "width": 2, "height": 9 },
"m": { "x": 78, "y": 36, "width": 6, "height": 9 },
"n": { "x": 84, "y": 36, "width": 5, "height": 9 },
"o": { "x": 90, "y": 36, "width": 6, "height": 9 },
"p": { "x": 0, "y": 45, "width": 6, "height": 9 },
"q": { "x": 6, "y": 45, "width": 6, "height": 9 },
"r": { "x": 12, "y": 45, "width": 6, "height": 9 },
"s": { "x": 18, "y": 45, "width": 6, "height": 9 },
"t": { "x": 24, "y": 45, "width": 5, "height": 9 },
"u": { "x": 30, "y": 45, "width": 5, "height": 9 },
"v": { "x": 36, "y": 45, "width": 6, "height": 9 },
"w": { "x": 42, "y": 45, "width": 6, "height": 9 },
"x": { "x": 48, "y": 45, "width": 6, "height": 9 },
"y": { "x": 54, "y": 45, "width": 6, "height": 9 },
"z": { "x": 60, "y": 45, "width": 5, "height": 9 },
"{": { "x": 66, "y": 45, "width": 4, "height": 9 },
"|": { "x": 72, "y": 45, "width": 2, "height": 9 },
"}": { "x": 78, "y": 45, "width": 4, "height": 9 },
"~": { "x": 84, "y": 45, "width": 6, "height": 9 },
"\u007f": { "x": 90, "y": 45, "width": 6, "height": 9 },
"\u0080": { "x": 0, "y": 54, "width": 6, "height": 9 },
"\u0081": { "x": 6, "y": 54, "width": 6, "height": 9 },
"\u0082": { "x": 12, "y": 54, "width": 4, "height": 9 },
"\u0083": { "x": 18, "y": 54, "width": 4, "height": 9 },
"\u0084": { "x": 24, "y": 54, "width": 6, "height": 9 },
"\u0085": { "x": 30, "y": 54, "width": 6, "height": 9 },
"\u0086": { "x": 36, "y": 54, "width": 6, "height": 9 },
"\u0087": { "x": 42, "y": 54, "width": 6, "height": 9 },
"\u0090": { "x": 0, "y": 63, "width": 7, "height": 9, "icon": true },
"\u0091": { "x": 12, "y": 63, "width": 7, "height": 9, "icon": true },
"\u0092": { "x": 24, "y": 63, "width": 9, "height": 9, "icon": true },
"\u0093": { "x": 36, "y": 63, "width": 8, "height": 10, "icon": true },
"\u0094": { "x": 48, "y": 63, "width": 11, "height": 10, "icon": true },
"\u0095": { "x": 60, "y": 63, "width": 12, "height": 10, "icon": true },
"\u0096": { "x": 72, "y": 63, "width": 14, "height": 9, "icon": true },
"\u00a0": { "x": 0, "y": 73, "width": 10, "height": 10, "icon": true },
"\u00a1": { "x": 12, "y": 73, "width": 10, "height": 10, "icon": true },
"\u00a2": { "x": 24, "y": 73, "width": 10, "height": 10, "icon": true },
"\u00a3": { "x": 36, "y": 73, "width": 10, "height": 9, "icon": true },
"\u00a4": { "x": 48, "y": 73, "width": 10, "height": 9, "icon": true },
"\u00a5": { "x": 60, "y": 73, "width": 10, "height": 10, "icon": true }
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -19,6 +19,11 @@
"source": "${PROJECT_BINARY_DIR}/launcher803fd000.psexe" "source": "${PROJECT_BINARY_DIR}/launcher803fd000.psexe"
}, },
{
"type": "strings",
"name": "assets/lang/en.lang",
"source": "${PROJECT_SOURCE_DIR}/assets/lang/en.json"
},
{ {
"type": "tim", "type": "tim",
"name": "assets/textures/background.tim", "name": "assets/textures/background.tim",
@ -32,21 +37,21 @@
"name": "assets/textures/font.tim", "name": "assets/textures/font.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/font.png", "source": "${PROJECT_SOURCE_DIR}/assets/textures/font.png",
"quantize": 16, "quantize": 16,
"imagePos": { "x": 984, "y": 0 }, "imagePos": { "x": 960, "y": 126 },
"clutPos": { "x": 1008, "y": 1 } "clutPos": { "x": 1008, "y": 1 }
}, },
{ {
"type": "tim", "type": "tim",
"name": "assets/textures/splash.tim", "name": "assets/textures/splash.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/splash.png", "source": "${PROJECT_SOURCE_DIR}/assets/textures/splash.png",
"quantize": 16, "quantize": 16,
"imagePos": { "x": 960, "y": 96 }, "imagePos": { "x": 984, "y": 0 },
"clutPos": { "x": 1008, "y": 2 } "clutPos": { "x": 1008, "y": 2 }
}, },
{ {
"type": "metrics", "type": "metrics",
"name": "assets/textures/font.metrics", "name": "assets/textures/font.metrics",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/font.metrics.json" "source": "${PROJECT_SOURCE_DIR}/assets/textures/font.json"
}, },
{ {
"type": "binary", "type": "binary",
@ -96,21 +101,17 @@
"source": "${PROJECT_SOURCE_DIR}/assets/sounds/screenshot.vag", "source": "${PROJECT_SOURCE_DIR}/assets/sounds/screenshot.vag",
"compression": "none" "compression": "none"
}, },
{
"type": "palette",
"name": "assets/app.palette",
"source": "${PROJECT_SOURCE_DIR}/assets/app.palette.json"
},
{
"type": "strings",
"name": "assets/app.strings",
"source": "${PROJECT_SOURCE_DIR}/assets/app.strings.json"
},
{ {
"type": "text", "type": "text",
"name": "assets/about.txt", "name": "assets/about.txt",
"source": "${PROJECT_BINARY_DIR}/about.txt" "source": "${PROJECT_BINARY_DIR}/about.txt"
}, },
{
"type": "palette",
"name": "assets/palette.dat",
"source": "${PROJECT_SOURCE_DIR}/assets/palette.json",
"compression": "none"
},
{ {
"type": "binary", "type": "binary",

View File

@ -19,6 +19,11 @@
"source": "${PROJECT_BINARY_DIR}/launcher803fd000.psexe" "source": "${PROJECT_BINARY_DIR}/launcher803fd000.psexe"
}, },
{
"type": "strings",
"name": "assets/lang/en.lang",
"source": "${PROJECT_SOURCE_DIR}/assets/lang/en.json"
},
{ {
"type": "tim", "type": "tim",
"name": "assets/textures/background.tim", "name": "assets/textures/background.tim",
@ -32,21 +37,21 @@
"name": "assets/textures/font.tim", "name": "assets/textures/font.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/font.png", "source": "${PROJECT_SOURCE_DIR}/assets/textures/font.png",
"quantize": 16, "quantize": 16,
"imagePos": { "x": 984, "y": 0 }, "imagePos": { "x": 960, "y": 126 },
"clutPos": { "x": 1008, "y": 1 } "clutPos": { "x": 1008, "y": 1 }
}, },
{ {
"type": "tim", "type": "tim",
"name": "assets/textures/splash.tim", "name": "assets/textures/splash.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/splash.png", "source": "${PROJECT_SOURCE_DIR}/assets/textures/splash.png",
"quantize": 16, "quantize": 16,
"imagePos": { "x": 960, "y": 96 }, "imagePos": { "x": 984, "y": 0 },
"clutPos": { "x": 1008, "y": 2 } "clutPos": { "x": 1008, "y": 2 }
}, },
{ {
"type": "metrics", "type": "metrics",
"name": "assets/textures/font.metrics", "name": "assets/textures/font.metrics",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/font.metrics.json" "source": "${PROJECT_SOURCE_DIR}/assets/textures/font.json"
}, },
{ {
"type": "binary", "type": "binary",
@ -90,25 +95,16 @@
"source": "${PROJECT_SOURCE_DIR}/assets/sounds/screenshot.vag", "source": "${PROJECT_SOURCE_DIR}/assets/sounds/screenshot.vag",
"compression": "none" "compression": "none"
}, },
{
"type": "palette",
"name": "assets/app.palette",
"source": "${PROJECT_SOURCE_DIR}/assets/app.palette.json"
},
{
"type": "strings",
"name": "assets/app.strings",
"source": "${PROJECT_SOURCE_DIR}/assets/app.strings.json"
},
{ {
"type": "text", "type": "text",
"name": "assets/about.txt", "name": "assets/about.txt",
"source": "${PROJECT_BINARY_DIR}/about.txt" "source": "${PROJECT_BINARY_DIR}/about.txt"
}, },
{ {
"type": "db", "type": "palette",
"name": "data/games.db", "name": "assets/palette.dat",
"source": "${PROJECT_SOURCE_DIR}/data/games.json" "source": "${PROJECT_SOURCE_DIR}/assets/palette.json",
"compression": "none"
}, },
{ {
@ -116,6 +112,12 @@
"name": "data/fpga.bit", "name": "data/fpga.bit",
"source": "${PROJECT_SOURCE_DIR}/data/fpga.bit" "source": "${PROJECT_SOURCE_DIR}/data/fpga.bit"
}, },
{
"type": "db",
"name": "data/games.db",
"source": "${PROJECT_SOURCE_DIR}/data/games.json"
},
{ {
"type": "binary", "type": "binary",
"name": "data/x76f041.db", "name": "data/x76f041.db",

View File

@ -5,7 +5,13 @@
"title": "Root", "title": "Root",
"type": "object", "type": "object",
"required": [ "spaceWidth", "tabWidth", "lineHeight", "characterSizes" ], "required": [
"spaceWidth",
"tabWidth",
"lineHeight",
"baselineOffset",
"characterSizes"
],
"properties": { "properties": {
"spaceWidth": { "spaceWidth": {
@ -20,13 +26,18 @@
}, },
"lineHeight": { "lineHeight": {
"title": "Line height", "title": "Line height",
"description": "Height of each line in pixels, including any padding. Note that characters whose height is lower than this value will be aligned to the top of the line, rather than the bottom.", "description": "Height of each line in pixels, including any padding. Note that characters whose height is lower than this value will be aligned to the top of the line by default.",
"type": "integer"
},
"baselineOffset": {
"title": "Baseline offset",
"description": "Offset to add to the Y coordinate of each line. Can be negative.",
"type": "integer" "type": "integer"
}, },
"characterSizes": { "characterSizes": {
"title": "Character list", "title": "Character list",
"description": "List of all glyphs in the texture. Each entry's key must be a single-character string containing a printable ASCII or extended (\\u0080-\\u00ff) character.", "description": "List of all glyphs in the texture. Each entry's key must be a single-character string containing a printable Unicode character.",
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,

View File

@ -62,6 +62,8 @@
"oneOf": [ "oneOf": [
{ {
"additionalProperties": false,
"properties": { "properties": {
"type": { "const": "empty" }, "type": { "const": "empty" },
@ -75,11 +77,14 @@
} }
}, },
{ {
"required": [ "source" ], "required": [ "source" ],
"additionalProperties": false,
"properties": { "properties": {
"type": { "pattern": "^text|binary$" }, "type": { "pattern": "^text|binary$" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"source": { "source": {
"title": "Path to source file", "title": "Path to source file",
@ -95,8 +100,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "tim" }, "type": { "const": "tim" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"source": { "source": {
"title": "Path to source file", "title": "Path to source file",
@ -174,8 +181,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "pattern": "^metrics|palette|strings|db$" }, "type": { "pattern": "^metrics|palette|strings|db$" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"source": { "source": {
"title": "Path to source file", "title": "Path to source file",
@ -191,8 +200,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "metrics" }, "type": { "const": "metrics" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"metrics": { "metrics": {
"title": "Font metrics", "title": "Font metrics",
@ -206,8 +217,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "palette" }, "type": { "const": "palette" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"palette": { "palette": {
"title": "Color entries", "title": "Color entries",
@ -221,8 +234,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "strings" }, "type": { "const": "strings" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"strings": { "strings": {
"title": "String table", "title": "String table",
@ -236,8 +251,10 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"type": { "const": "db" }, "type": { "const": "db" },
"name": { "type": "string" }, "name": { "type": "string" },
"compression": {},
"compressLevel": {},
"strings": { "strings": {
"title": "Game database", "title": "Game database",

View File

@ -30,28 +30,28 @@
#define VERSION_STRING VERSION "-debug" #define VERSION_STRING VERSION "-debug"
#endif #endif
enum Character : char { #define CH_UP_ARROW "\u25b4"
CH_UP_ARROW = '\x80', #define CH_DOWN_ARROW "\u25be"
CH_DOWN_ARROW = '\x81', #define CH_LEFT_ARROW "\u25c2"
CH_LEFT_ARROW = '\x82', #define CH_RIGHT_ARROW "\u25b8"
CH_RIGHT_ARROW = '\x83', #define CH_UP_ARROW_ALT "\u2191"
CH_UP_ARROW_ALT = '\x84', #define CH_DOWN_ARROW_ALT "\u2193"
CH_DOWN_ARROW_ALT = '\x85', #define CH_LEFT_ARROW_ALT "\u2190"
CH_LEFT_ARROW_ALT = '\x86', #define CH_RIGHT_ARROW_ALT "\u2192"
CH_RIGHT_ARROW_ALT = '\x87', #define CH_INVALID_CHAR "\ufffd"
CH_LEFT_BUTTON = '\x90', #define CH_LEFT_BUTTON "\u25c1"
CH_RIGHT_BUTTON = '\x91', #define CH_RIGHT_BUTTON "\u25b7"
CH_START_BUTTON = '\x92', #define CH_START_BUTTON "\u25ad"
CH_CLOSED_LOCK = '\x93', #define CH_CLOSED_LOCK "\U0001f512"
CH_OPEN_LOCK = '\x94', #define CH_OPEN_LOCK "\U0001f513"
CH_CHIP_ICON = '\x95', #define CH_CIRCLE_BUTTON "\u25cb"
CH_CART_ICON = '\x96', #define CH_X_BUTTON "\u2715"
#define CH_TRIANGLE_BUTTON "\u25b3"
CH_CDROM_ICON = '\xa0', #define CH_SQUARE_BUTTON "\u25a1"
CH_HDD_ICON = '\xa1', #define CH_CDROM_ICON "\U0001f5b8"
CH_HOST_ICON = '\xa2', #define CH_HDD_ICON "\U0001f5b4"
CH_DIR_ICON = '\xa3', #define CH_HOST_ICON "\U0001f5a7"
CH_PARENT_DIR_ICON = '\xa4', #define CH_DIR_ICON "\U0001f5c0"
CH_FILE_ICON = '\xa5' #define CH_PARENT_DIR_ICON "\U0001f5bf"
}; #define CH_FILE_ICON "\U0001f5ce"

View File

@ -244,20 +244,17 @@ const char *StringTable::get(util::Hash id) const {
if (!ptr) if (!ptr)
return _ERROR_STRING; return _ERROR_STRING;
auto blob = reinterpret_cast<const char *>(ptr); auto blob = as<const char>();
auto table = reinterpret_cast<const StringTableEntry *>(ptr); auto table = as<const StringTableEntry>();
auto index = id % STRING_TABLE_BUCKET_COUNT;
auto entry = &table[id % TABLE_BUCKET_COUNT]; do {
auto entry = &table[index];
if (entry->hash == id) index = entry->chained;
return &blob[entry->offset];
while (entry->chained) {
entry = &table[entry->chained];
if (entry->hash == id) if (entry->hash == id)
return &blob[entry->offset]; return &blob[entry->offset];
} } while (index);
return _ERROR_STRING; return _ERROR_STRING;
} }

View File

@ -159,7 +159,7 @@ public:
/* String table parser */ /* String table parser */
static constexpr int TABLE_BUCKET_COUNT = 256; static constexpr size_t STRING_TABLE_BUCKET_COUNT = 256;
struct StringTableEntry { struct StringTableEntry {
public: public:

View File

@ -15,40 +15,72 @@
*/ */
#include <stdint.h> #include <stdint.h>
#include "common/util/string.hpp"
#include "common/gpu.hpp" #include "common/gpu.hpp"
#include "common/gpufont.hpp" #include "common/gpufont.hpp"
#include "ps1/gpucmd.h" #include "ps1/gpucmd.h"
namespace gpu { namespace gpu {
/* Font metrics class */
CharacterSize FontMetrics::get(util::UTF8CodePoint id) const {
if (!ptr)
return 0;
auto table = reinterpret_cast<const FontMetricsEntry *>(getHeader() + 1);
auto index = id % METRICS_BUCKET_COUNT;
do {
auto entry = &table[index];
index = entry->getChained();
if (entry->getCodePoint() == id)
return entry->size;
} while (index);
return (id == FONT_INVALID_CHAR) ? 0 : get(FONT_INVALID_CHAR);
}
/* Font class */ /* Font class */
void Font::draw( void Font::draw(
Context &ctx, const char *str, const Rect &rect, const Rect &clipRect, Context &ctx, const char *str, const Rect &rect, const Rect &clipRect,
Color color, bool wordWrap Color color, bool wordWrap
) const { ) const {
// This is required for non-ASCII characters to work properly. if (!str || !metrics.ptr)
auto _str = reinterpret_cast<const uint8_t *>(str);
if (!str)
return; return;
ctx.setTexturePage(image.texpage); ctx.setTexturePage(image.texpage);
int x = rect.x1, y = rect.y1; auto header = metrics.getHeader();
for (uint8_t ch = *_str; ch; ch = *(++_str)) { int x = rect.x1;
int clipX1 = clipRect.x1;
int clipX2 = clipRect.x2;
int y = rect.y1 + header->baselineOffset;
int clipY1 = clipRect.y1 + header->baselineOffset;
int clipY2 = clipRect.y2 + header->baselineOffset;
int rectY2 = rect.y2 + header->baselineOffset - header->lineHeight;
for (;;) {
auto ch = util::parseUTF8Character(str);
bool wrap = wordWrap; bool wrap = wordWrap;
str += ch.length;
switch (ch.codePoint) {
case 0:
return;
switch (ch) {
case '\t': case '\t':
x += metrics.tabWidth; x += header->tabWidth;
x -= x % metrics.tabWidth; x -= x % header->tabWidth;
break; break;
case '\n': case '\n':
x = rect.x1; x = rect.x1;
y += metrics.lineHeight; y += header->lineHeight;
break; break;
case '\r': case '\r':
@ -56,26 +88,25 @@ void Font::draw(
break; break;
case ' ': case ' ':
x += metrics.spaceWidth; x += header->spaceWidth;
break; break;
default: default:
uint32_t size = metrics.getCharacterSize(ch); auto size = metrics.get(ch.codePoint);
int u = size & 0xff; size >>= 8; int u = size & 0xff; size >>= 8;
int v = size & 0xff; size >>= 8; int v = size & 0xff; size >>= 8;
int w = size & 0x7f; size >>= 7; int w = size & 0x7f; size >>= 7;
int h = size & 0x7f; size >>= 7; int h = size & 0x7f; size >>= 7;
if (y > clipRect.y2) if (y > clipY2)
return; return;
if ( if (
(x >= (clipRect.x1 - w)) && (x <= clipRect.x2) && (x >= (clipX1 - w)) && (x <= clipX2) && (y >= (clipY1 - h))
(y >= (clipRect.y1 - h))
) { ) {
auto cmd = ctx.newPacket(4); auto cmd = ctx.newPacket(4);
cmd[0] = color | gp0_rectangle(true, size, true); cmd[0] = color | gp0_rectangle(true, size & 1, true);
cmd[1] = gp0_xy(x, y); cmd[1] = gp0_xy(x, y);
cmd[2] = gp0_uv(u + image.u, v + image.v, image.palette); cmd[2] = gp0_uv(u + image.u, v + image.v, image.palette);
cmd[3] = gp0_xy(w, h); cmd[3] = gp0_xy(w, h);
@ -88,16 +119,15 @@ void Font::draw(
// Handle word wrapping by calculating the length of the next word and // Handle word wrapping by calculating the length of the next word and
// checking if it can still fit in the current line. // checking if it can still fit in the current line.
int boundaryX = rect.x2; int boundaryX = rect.x2;
if (wrap) if (wrap)
boundaryX -= getStringWidth( boundaryX -= getStringWidth(str, true);
reinterpret_cast<const char *>(&_str[1]), true
);
if (x > boundaryX) { if (x > boundaryX) {
x = rect.x1; x = rect.x1;
y += metrics.lineHeight; y += header->lineHeight;
} }
if (y > (rect.y2 - metrics.lineHeight)) if (y > rectY2)
return; return;
} }
} }
@ -122,7 +152,9 @@ void Font::draw(
draw(ctx, str, _rect, color, wordWrap); draw(ctx, str, _rect, color, wordWrap);
} }
int Font::getCharacterWidth(char ch) const { int Font::getCharacterWidth(util::UTF8CodePoint ch) const {
auto header = metrics.getHeader();
switch (ch) { switch (ch) {
case 0: case 0:
case '\n': case '\n':
@ -130,36 +162,43 @@ int Font::getCharacterWidth(char ch) const {
return 0; return 0;
case '\t': case '\t':
return metrics.tabWidth; return header->tabWidth;
case ' ': case ' ':
return metrics.spaceWidth; return header->spaceWidth;
default: default:
return (metrics.getCharacterSize(ch) >> 16) & 0x7f; auto size = metrics.get(ch);
return (size >> 16) & 0x7f;
} }
} }
void Font::getStringBounds( void Font::getStringBounds(
const char *str, Rect &rect, bool wordWrap, bool breakOnSpace const char *str, Rect &rect, bool wordWrap, bool breakOnSpace
) const { ) const {
auto _str = reinterpret_cast<const uint8_t *>(str); if (!str || !metrics.ptr)
if (!str)
return; return;
auto header = metrics.getHeader();
int x = rect.x1, maxX = rect.x1, y = rect.y1; int x = rect.x1, maxX = rect.x1, y = rect.y1;
for (uint8_t ch = *_str; ch; ch = *(++_str)) { for (;;) {
auto ch = util::parseUTF8Character(str);
bool wrap = wordWrap; bool wrap = wordWrap;
str += ch.length;
switch (ch.codePoint) {
case 0:
goto _break;
switch (ch) {
case '\t': case '\t':
if (breakOnSpace) if (breakOnSpace)
goto _break; goto _break;
x += metrics.tabWidth; x += header->tabWidth;
x -= x % metrics.tabWidth; x -= x % header->tabWidth;
break; break;
case '\n': case '\n':
@ -169,7 +208,7 @@ void Font::getStringBounds(
maxX = x; maxX = x;
x = rect.x1; x = rect.x1;
y += metrics.lineHeight; y += header->lineHeight;
break; break;
case '\r': case '\r':
@ -185,51 +224,59 @@ void Font::getStringBounds(
if (breakOnSpace) if (breakOnSpace)
goto _break; goto _break;
x += metrics.spaceWidth; x += header->spaceWidth;
break; break;
default: default:
x += (metrics.getCharacterSize(ch) >> 16) & 0x7f; auto size = metrics.get(ch.codePoint);
x += (size >> 16) & 0x7f;
wrap = false; wrap = false;
} }
int boundaryX = rect.x2; int boundaryX = rect.x2;
if (wrap) if (wrap)
boundaryX -= getStringWidth( boundaryX -= getStringWidth(str, true);
reinterpret_cast<const char *>(&_str[1]), true
);
if (x > boundaryX) { if (x > boundaryX) {
if (x > maxX) if (x > maxX)
maxX = x; maxX = x;
x = rect.x1; x = rect.x1;
y += metrics.lineHeight; y += header->lineHeight;
} }
if (y > (rect.y2 - metrics.lineHeight)) if (y > (rect.y2 - header->lineHeight))
goto _break; goto _break;
} }
_break: _break:
rect.x2 = maxX; rect.x2 = maxX;
rect.y2 = y + metrics.lineHeight; rect.y2 = y + header->lineHeight;
} }
int Font::getStringWidth(const char *str, bool breakOnSpace) const { int Font::getStringWidth(const char *str, bool breakOnSpace) const {
auto _str = reinterpret_cast<const uint8_t *>(str); if (!str || !metrics.ptr)
if (!str)
return 0; return 0;
auto header = metrics.getHeader();
int width = 0, maxWidth = 0; int width = 0, maxWidth = 0;
for (uint8_t ch = *_str; ch; ch = *(++_str)) { for (;;) {
switch (ch) { auto ch = util::parseUTF8Character(str);
str += ch.length;
switch (ch.codePoint) {
case 0:
goto _break;
case '\t': case '\t':
if (breakOnSpace) if (breakOnSpace)
goto _break; goto _break;
width += metrics.tabWidth; width += header->tabWidth;
width -= width % metrics.tabWidth; width -= width % header->tabWidth;
break; break;
case '\n': case '\n':
@ -246,11 +293,11 @@ int Font::getStringWidth(const char *str, bool breakOnSpace) const {
if (breakOnSpace) if (breakOnSpace)
goto _break; goto _break;
width += metrics.spaceWidth; width += header->spaceWidth;
break; break;
default: default:
width += (metrics.getCharacterSize(ch) >> 16) & 0x7f; width += (metrics.get(ch.codePoint) >> 16) & 0x7f;
} }
} }

View File

@ -16,34 +16,75 @@
#pragma once #pragma once
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "common/gpufont.hpp"
#include "common/util/string.hpp"
#include "common/util/templates.hpp"
#include "common/gpu.hpp" #include "common/gpu.hpp"
namespace gpu { namespace gpu {
/* Font class */ /* Font metrics class */
static constexpr char FONT_INVALID_CHAR = 0x7f; static constexpr size_t METRICS_BUCKET_COUNT = 256;
static constexpr size_t METRICS_CODE_POINT_BITS = 21;
class FontMetrics { static constexpr util::UTF8CodePoint FONT_INVALID_CHAR = 0xfffd;
using CharacterSize = uint32_t;
struct FontMetricsHeader {
public: public:
uint8_t spaceWidth, tabWidth, lineHeight, _reserved; uint8_t spaceWidth, tabWidth, lineHeight;
uint32_t characterSizes[256]; int8_t baselineOffset;
};
inline uint32_t getCharacterSize(uint8_t ch) const { struct FontMetricsEntry {
uint32_t sizes = characterSizes[ch]; public:
if (!sizes) uint32_t codePoint;
return characterSizes[int(FONT_INVALID_CHAR)]; CharacterSize size;
return sizes; inline util::UTF8CodePoint getCodePoint(void) const {
return codePoint & ((1 << METRICS_CODE_POINT_BITS) - 1);
}
inline uint32_t getChained(void) const {
return codePoint >> METRICS_CODE_POINT_BITS;
} }
}; };
class FontMetrics : public util::Data {
public:
inline const FontMetricsHeader *getHeader(void) const {
return as<const FontMetricsHeader>();
}
inline CharacterSize operator[](util::UTF8CodePoint id) const {
return get(id);
}
CharacterSize get(util::UTF8CodePoint id) const;
};
/* Font class */
class Font { class Font {
public: public:
Image image; Image image;
FontMetrics metrics; FontMetrics metrics;
inline int getSpaceWidth(void) const {
if (!metrics.ptr)
return 0;
return metrics.getHeader()->spaceWidth;
}
inline int getLineHeight(void) const {
if (!metrics.ptr)
return 0;
return metrics.getHeader()->lineHeight;
}
void draw( void draw(
Context &ctx, const char *str, const Rect &rect, const Rect &clipRect, Context &ctx, const char *str, const Rect &rect, const Rect &clipRect,
Color color = 0x808080, bool wordWrap = false Color color = 0x808080, bool wordWrap = false
@ -56,7 +97,7 @@ public:
Context &ctx, const char *str, const RectWH &rect, Context &ctx, const char *str, const RectWH &rect,
Color color = 0x808080, bool wordWrap = false Color color = 0x808080, bool wordWrap = false
) const; ) const;
int getCharacterWidth(char ch) const; int getCharacterWidth(util::UTF8CodePoint ch) const;
void getStringBounds( void getStringBounds(
const char *str, Rect &rect, bool wordWrap = false, const char *str, Rect &rect, bool wordWrap = false,
bool breakOnSpace = false bool breakOnSpace = false

View File

@ -69,6 +69,7 @@ public:
bool enable = disableInterrupts(); bool enable = disableInterrupts();
//assert(enable); //assert(enable);
(void) enable;
} }
inline ~ThreadCriticalSection(void) { inline ~ThreadCriticalSection(void) {
enableInterrupts(); enableInterrupts();

View File

@ -110,6 +110,24 @@ size_t encodeBase41(char *output, const uint8_t *input, size_t length) {
return outLength; return outLength;
} }
/* UTF-8 parser */
size_t getUTF8StringLength(const char *str) {
for (size_t length = 0;; length++) {
auto value = parseUTF8Character(str);
if (!value.length) { // Invalid character
str++;
continue;
}
if (!value.codePoint) // Null character
return length;
str += value.length;
}
}
/* LZ4 decompressor */ /* LZ4 decompressor */
void decompressLZ4( void decompressLZ4(

View File

@ -33,6 +33,28 @@ size_t serialNumberToString(char *output, const uint8_t *input);
size_t traceIDToString(char *output, const uint8_t *input); size_t traceIDToString(char *output, const uint8_t *input);
size_t encodeBase41(char *output, const uint8_t *input, size_t length); size_t encodeBase41(char *output, const uint8_t *input, size_t length);
/* UTF-8 parser */
using UTF8CodePoint = uint32_t;
struct UTF8Character {
public:
UTF8CodePoint codePoint;
size_t length;
};
extern "C" uint64_t _parseUTF8Character(const char *ch);
size_t getUTF8StringLength(const char *str);
static inline UTF8Character parseUTF8Character(const char *ch) {
auto values = _parseUTF8Character(ch);
return {
.codePoint = UTF8CodePoint(values),
.length = size_t(values >> 32)
};
}
/* LZ4 decompressor */ /* LZ4 decompressor */
static inline size_t getLZ4InPlaceMargin(size_t inputLength) { static inline size_t getLZ4InPlaceMargin(size_t inputLength) {

92
src/common/util/string.s Normal file
View File

@ -0,0 +1,92 @@
# 573in1 - Copyright (C) 2022-2024 spicyjpeg
#
# 573in1 is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# 573in1 is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# 573in1. If not, see <https://www.gnu.org/licenses/>.
.set noreorder
.set GTE_LZCS, $30 # Leading zero count input
.set GTE_LZCR, $31 # Leading zero count output
## UTF-8 parser
.set codePoint, $v0
.set length, $v1
.set ch, $a0
.set startMask, $a1
.set contMask, $a2
.set contByte, $a3
.set temp, $t0
.set i, $t1
.section .text._parseUTF8Character, "ax", @progbits
.global _parseUTF8Character
.type _parseUTF8Character, @function
_parseUTF8Character:
# 1-byte character: 0xxxxxxx
# 2-byte character: 110xxxxx 10xxxxxx
# 3-byte character: 1110xxxx 10xxxxxx 10xxxxxx
# 4-byte character: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
# uint8_t codePoint = *(ch++);
lb codePoint, 0(ch)
addiu ch, 1
bltz codePoint, .LmsbSet
andi codePoint, 0xff
.LmsbNotSet: # if (signExtend(codePoint) >= 0)
# return { codePoint, 1 };
jr $ra
li length, 1
.LmsbSet: # if (signExtend(codePoint) < 0)
# size_t length = countLeadingOnes(codePoint << 24);
sll temp, codePoint, 24
mtc2 temp, GTE_LZCS
li contMask, (1 << 7)
nop
mfc2 length, GTE_LZCR
li startMask, (1 << 7) - 1
# codePoint &= (1 << (7 - length)) - 1;
srlv startMask, startMask, length
and codePoint, startMask
addiu i, length, -2
.LcontinuationLoop: # for (size_t i = length - 1; i--;)
# uint8_t contByte = *(ch++);
lbu contByte, 0(ch)
addiu ch, 1
# if ((contByte & 0xc0) != 0x80) goto returnInvalid;
andi temp, contByte, (3 << 6)
bne temp, contMask, .LreturnInvalid
andi contByte, (1 << 6) - 1
# codePoint <<= 6;
# codePoint |= contByte & 0x3f;
sll codePoint, 6
or codePoint, contByte
bnez i, .LcontinuationLoop
addiu i, -1
.LreturnValid:
# return { codePoint, length };
jr $ra
nop
.LreturnInvalid:
# return { codePoint, 0 };
jr $ra
li length, 0

View File

@ -243,12 +243,12 @@ static const char *const _UI_SOUND_PATHS[ui::NUM_UI_SOUNDS]{
void App::_loadResources(void) { void App::_loadResources(void) {
auto &res = _fileIO.resource; auto &res = _fileIO.resource;
res.loadStruct(_ctx.colors, "assets/palette.dat");
res.loadTIM(_background.tile, "assets/textures/background.tim"); res.loadTIM(_background.tile, "assets/textures/background.tim");
res.loadTIM(_ctx.font.image, "assets/textures/font.tim"); res.loadTIM(_ctx.font.image, "assets/textures/font.tim");
res.loadStruct(_ctx.font.metrics, "assets/textures/font.metrics"); res.loadData(_ctx.font.metrics, "assets/textures/font.metrics");
res.loadTIM(_splashOverlay.image, "assets/textures/splash.tim"); res.loadTIM(_splashOverlay.image, "assets/textures/splash.tim");
res.loadStruct(_ctx.colors, "assets/app.palette"); res.loadData(_stringTable, "assets/lang/en.lang");
res.loadData(_stringTable, "assets/app.strings");
file::currentSPUOffset = spu::DUMMY_BLOCK_END; file::currentSPUOffset = spu::DUMMY_BLOCK_END;

View File

@ -152,14 +152,14 @@ const char *FilePickerScreen::_getItemName(ui::Context &ctx, int index) const {
auto &dev = ide::devices[drive]; auto &dev = ide::devices[drive];
auto fs = APP->_fileIO.ide[drive]; auto fs = APP->_fileIO.ide[drive];
auto icon = (dev.flags & ide::DEVICE_ATAPI) auto format = (dev.flags & ide::DEVICE_ATAPI)
? CH_CDROM_ICON ? (CH_CDROM_ICON " %s: %s")
: CH_HDD_ICON; : (CH_HDD_ICON " %s: %s");
auto label = fs auto label = fs
? fs->volumeLabel ? fs->volumeLabel
: STR("FilePickerScreen.noFS"); : STR("FilePickerScreen.noFS");
snprintf(name, sizeof(name), "%c %s: %s", icon, dev.model, label); snprintf(name, sizeof(name), format, dev.model, label);
return name; return name;
} }
@ -310,26 +310,24 @@ const char *FileBrowserScreen::_getItemName(ui::Context &ctx, int index) const {
if (!_isRoot) if (!_isRoot)
index--; index--;
const char *path; const char *format, *path;
if (index < 0) { if (index < 0) {
name[0] = CH_PARENT_DIR_ICON; format = CH_PARENT_DIR_ICON " %s";
path = STR("FileBrowserScreen.parentDir"); path = STR("FileBrowserScreen.parentDir");
} else if (index < _numDirectories) { } else if (index < _numDirectories) {
auto entries = _directories.as<file::FileInfo>(); auto entries = _directories.as<file::FileInfo>();
name[0] = CH_DIR_ICON; format = CH_DIR_ICON " %s";
path = entries[index].name; path = entries[index].name;
} else { } else {
auto entries = _files.as<file::FileInfo>(); auto entries = _files.as<file::FileInfo>();
name[0] = CH_FILE_ICON; format = CH_FILE_ICON " %s";
path = entries[index - _numDirectories].name; path = entries[index - _numDirectories].name;
} }
name[1] = ' '; snprintf(name, sizeof(name), format, path);
__builtin_strncpy(&name[2], path, sizeof(name) - 2);
return name; return name;
} }

View File

@ -275,6 +275,7 @@ void TestPatternScreen::_drawTextOverlay(
) const { ) const {
int screenWidth = ctx.gpuCtx.width - ui::SCREEN_MARGIN_X * 2; int screenWidth = ctx.gpuCtx.width - ui::SCREEN_MARGIN_X * 2;
int screenHeight = ctx.gpuCtx.height - ui::SCREEN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - ui::SCREEN_MARGIN_Y * 2;
int lineHeight = ctx.font.getLineHeight();
_newLayer(ctx, 0, 0, ctx.gpuCtx.width, ctx.gpuCtx.height); _newLayer(ctx, 0, 0, ctx.gpuCtx.width, ctx.gpuCtx.height);
@ -283,7 +284,7 @@ void TestPatternScreen::_drawTextOverlay(
backdropRect.x = ui::SCREEN_MARGIN_X - ui::SHADOW_OFFSET; backdropRect.x = ui::SCREEN_MARGIN_X - ui::SHADOW_OFFSET;
backdropRect.y = ui::SCREEN_MARGIN_Y - ui::SHADOW_OFFSET; backdropRect.y = ui::SCREEN_MARGIN_Y - ui::SHADOW_OFFSET;
backdropRect.w = ui::SHADOW_OFFSET * 2 + screenWidth; backdropRect.w = ui::SHADOW_OFFSET * 2 + screenWidth;
backdropRect.h = ui::SHADOW_OFFSET * 2 + ctx.font.metrics.lineHeight; backdropRect.h = ui::SHADOW_OFFSET * 2 + lineHeight;
ctx.gpuCtx.drawRect(backdropRect, ctx.colors[ui::COLOR_SHADOW], true); ctx.gpuCtx.drawRect(backdropRect, ctx.colors[ui::COLOR_SHADOW], true);
backdropRect.y += screenHeight - ui::SCREEN_PROMPT_HEIGHT_MIN; backdropRect.y += screenHeight - ui::SCREEN_PROMPT_HEIGHT_MIN;
@ -294,7 +295,7 @@ void TestPatternScreen::_drawTextOverlay(
textRect.x1 = ui::SCREEN_MARGIN_X; textRect.x1 = ui::SCREEN_MARGIN_X;
textRect.y1 = ui::SCREEN_MARGIN_Y; textRect.y1 = ui::SCREEN_MARGIN_Y;
textRect.x2 = textRect.x1 + screenWidth; textRect.x2 = textRect.x1 + screenWidth;
textRect.y2 = textRect.y1 + ctx.font.metrics.lineHeight; textRect.y2 = textRect.y1 + lineHeight;
ctx.font.draw(ctx.gpuCtx, title, textRect, ctx.colors[ui::COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, title, textRect, ctx.colors[ui::COLOR_TITLE]);
textRect.y1 += screenHeight - ui::SCREEN_PROMPT_HEIGHT_MIN; textRect.y1 += screenHeight - ui::SCREEN_PROMPT_HEIGHT_MIN;
@ -345,18 +346,19 @@ static const IntensityBar _INTENSITY_BARS[]{
void ColorIntensityScreen::draw(ui::Context &ctx, bool active) const { void ColorIntensityScreen::draw(ui::Context &ctx, bool active) const {
TestPatternScreen::draw(ctx, active); TestPatternScreen::draw(ctx, active);
int barWidth = _INTENSITY_BAR_NAME_WIDTH + _INTENSITY_BAR_WIDTH; int barWidth = _INTENSITY_BAR_NAME_WIDTH + _INTENSITY_BAR_WIDTH;
int barHeight = _INTENSITY_BAR_HEIGHT * util::countOf(_INTENSITY_BARS); int barHeight = _INTENSITY_BAR_HEIGHT * util::countOf(_INTENSITY_BARS);
int offsetX = (ctx.gpuCtx.width - barWidth) / 2; int offsetX = (ctx.gpuCtx.width - barWidth) / 2;
int offsetY = (ctx.gpuCtx.height - barHeight) / 2; int offsetY = (ctx.gpuCtx.height - barHeight) / 2;
int lineHeight = ctx.font.getLineHeight();
gpu::RectWH textRect, barRect; gpu::RectWH textRect, barRect;
textRect.x = offsetX; textRect.x = offsetX;
textRect.y = textRect.y =
offsetY + (_INTENSITY_BAR_HEIGHT - ctx.font.metrics.lineHeight) / 2; offsetY + (_INTENSITY_BAR_HEIGHT - lineHeight) / 2;
textRect.w = _INTENSITY_BAR_NAME_WIDTH; textRect.w = _INTENSITY_BAR_NAME_WIDTH;
textRect.h = ctx.font.metrics.lineHeight; textRect.h = lineHeight;
barRect.x = offsetX + _INTENSITY_BAR_NAME_WIDTH; barRect.x = offsetX + _INTENSITY_BAR_NAME_WIDTH;
barRect.y = offsetY; barRect.y = offsetY;
@ -381,7 +383,7 @@ void ColorIntensityScreen::draw(ui::Context &ctx, bool active) const {
char value[2]{ 0, 0 }; char value[2]{ 0, 0 };
textRect.x = barRect.x + 1; textRect.x = barRect.x + 1;
textRect.y = offsetY - ctx.font.metrics.lineHeight; textRect.y = offsetY - lineHeight;
textRect.w = _INTENSITY_BAR_WIDTH / 32; textRect.w = _INTENSITY_BAR_WIDTH / 32;
for (int i = 0; i < 32; i++, textRect.x += textRect.w) { for (int i = 0; i < 32; i++, textRect.x += textRect.w) {

View File

@ -272,10 +272,12 @@ void TiledBackground::draw(Context &ctx, bool active) const {
} }
void TextOverlay::draw(Context &ctx, bool active) const { void TextOverlay::draw(Context &ctx, bool active) const {
int lineHeight = ctx.font.getLineHeight();
gpu::RectWH rect; gpu::RectWH rect;
rect.y = ctx.gpuCtx.height - (8 + ctx.font.metrics.lineHeight); rect.y = ctx.gpuCtx.height - (8 + lineHeight);
rect.h = ctx.font.metrics.lineHeight; rect.h = lineHeight;
if (leftText) { if (leftText) {
rect.x = 8; rect.x = 8;
@ -342,22 +344,22 @@ void LogOverlay::draw(Context &ctx, bool active) const {
// Text // Text
int screenHeight = ctx.gpuCtx.height - SCREEN_MIN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - SCREEN_MIN_MARGIN_Y * 2;
int linesShown = screenHeight / ctx.font.metrics.lineHeight; int lineHeight = ctx.font.getLineHeight();
gpu::Rect rect; gpu::Rect rect;
rect.x1 = SCREEN_MIN_MARGIN_X; rect.x1 = SCREEN_MIN_MARGIN_X;
rect.y1 = SCREEN_MIN_MARGIN_Y; rect.y1 = SCREEN_MIN_MARGIN_Y;
rect.x2 = ctx.gpuCtx.width - SCREEN_MIN_MARGIN_X; rect.x2 = ctx.gpuCtx.width - SCREEN_MIN_MARGIN_X;
rect.y2 = SCREEN_MIN_MARGIN_Y + ctx.font.metrics.lineHeight; rect.y2 = SCREEN_MIN_MARGIN_Y + lineHeight;
for (int i = linesShown - 1; i >= 0; i--) { for (int i = (screenHeight / lineHeight) - 1; i >= 0; i--) {
ctx.font.draw( ctx.font.draw(
ctx.gpuCtx, _buffer.getLine(i), rect, ctx.colors[COLOR_TEXT1] ctx.gpuCtx, _buffer.getLine(i), rect, ctx.colors[COLOR_TEXT1]
); );
rect.y1 = rect.y2; rect.y1 = rect.y2;
rect.y2 += ctx.font.metrics.lineHeight; rect.y2 += lineHeight;
} }
} }
@ -471,7 +473,7 @@ void ModalScreen::draw(Context &ctx, bool active) const {
rect.x1 = TITLE_BAR_PADDING; rect.x1 = TITLE_BAR_PADDING;
rect.y1 = TITLE_BAR_PADDING; rect.y1 = TITLE_BAR_PADDING;
rect.x2 = _width - TITLE_BAR_PADDING; rect.x2 = _width - TITLE_BAR_PADDING;
rect.y2 = TITLE_BAR_PADDING + ctx.font.metrics.lineHeight; rect.y2 = TITLE_BAR_PADDING + ctx.font.getLineHeight();
//rect.y2 = TITLE_BAR_HEIGHT - TITLE_BAR_PADDING; //rect.y2 = TITLE_BAR_HEIGHT - TITLE_BAR_PADDING;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);

View File

@ -38,6 +38,7 @@ void TextScreen::show(Context &ctx, bool goBack) {
void TextScreen::draw(Context &ctx, bool active) const { void TextScreen::draw(Context &ctx, bool active) const {
int screenWidth = ctx.gpuCtx.width - SCREEN_MARGIN_X * 2; int screenWidth = ctx.gpuCtx.width - SCREEN_MARGIN_X * 2;
int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
int lineHeight = ctx.font.getLineHeight();
// Top/bottom text // Top/bottom text
_newLayer( _newLayer(
@ -49,14 +50,14 @@ void TextScreen::draw(Context &ctx, bool active) const {
rect.x1 = 0; rect.x1 = 0;
rect.y1 = 0; rect.y1 = 0;
rect.x2 = screenWidth; rect.x2 = screenWidth;
rect.y2 = ctx.font.metrics.lineHeight; rect.y2 = lineHeight;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);
rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT_MIN; rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT_MIN;
rect.y2 = screenHeight; rect.y2 = screenHeight;
ctx.font.draw(ctx.gpuCtx, _prompt, rect, ctx.colors[COLOR_TEXT1], true); ctx.font.draw(ctx.gpuCtx, _prompt, rect, ctx.colors[COLOR_TEXT1], true);
int bodyOffset = ctx.font.metrics.lineHeight + SCREEN_BLOCK_MARGIN; int bodyOffset = lineHeight + SCREEN_BLOCK_MARGIN;
int bodyHeight = screenHeight - int bodyHeight = screenHeight -
(bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN); (bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN);
@ -82,7 +83,7 @@ void TextScreen::update(Context &ctx) {
return; return;
int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
int bodyOffset = ctx.font.metrics.lineHeight + SCREEN_BLOCK_MARGIN; int bodyOffset = ctx.font.getLineHeight() + SCREEN_BLOCK_MARGIN;
int bodyHeight = screenHeight - int bodyHeight = screenHeight -
(bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN); (bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN);
@ -128,6 +129,8 @@ _prompt(nullptr) {}
void ImageScreen::draw(Context &ctx, bool active) const { void ImageScreen::draw(Context &ctx, bool active) const {
_newLayer(ctx, 0, 0, ctx.gpuCtx.width, ctx.gpuCtx.height); _newLayer(ctx, 0, 0, ctx.gpuCtx.width, ctx.gpuCtx.height);
int lineHeight = ctx.font.getLineHeight();
if (_image) { if (_image) {
int x = ctx.gpuCtx.width / 2; int x = ctx.gpuCtx.width / 2;
int y = ctx.gpuCtx.height / 2; int y = ctx.gpuCtx.height / 2;
@ -135,7 +138,7 @@ void ImageScreen::draw(Context &ctx, bool active) const {
int height = _image->height * _imageScale / 2; int height = _image->height * _imageScale / 2;
if (_prompt) if (_prompt)
y -= (SCREEN_PROMPT_HEIGHT - ctx.font.metrics.lineHeight) / 2; y -= (SCREEN_PROMPT_HEIGHT - lineHeight) / 2;
// Backdrop // Backdrop
if (_imagePadding) { if (_imagePadding) {
@ -162,7 +165,7 @@ void ImageScreen::draw(Context &ctx, bool active) const {
rect.x1 = SCREEN_MARGIN_X; rect.x1 = SCREEN_MARGIN_X;
rect.y1 = SCREEN_MARGIN_Y; rect.y1 = SCREEN_MARGIN_Y;
rect.x2 = ctx.gpuCtx.width - SCREEN_MARGIN_X; rect.x2 = ctx.gpuCtx.width - SCREEN_MARGIN_X;
rect.y2 = SCREEN_MARGIN_Y + ctx.font.metrics.lineHeight; rect.y2 = SCREEN_MARGIN_Y + lineHeight;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);
rect.y1 = ctx.gpuCtx.height - (SCREEN_MARGIN_Y + SCREEN_PROMPT_HEIGHT); rect.y1 = ctx.gpuCtx.height - (SCREEN_MARGIN_Y + SCREEN_PROMPT_HEIGHT);
@ -177,6 +180,7 @@ void ListScreen::_drawItems(Context &ctx) const {
int itemY = _scrollAnim.getValue(ctx.time); int itemY = _scrollAnim.getValue(ctx.time);
int itemWidth = _getItemWidth(ctx); int itemWidth = _getItemWidth(ctx);
int listHeight = _getListHeight(ctx); int listHeight = _getListHeight(ctx);
int lineHeight = ctx.font.getLineHeight();
gpu::Rect rect; gpu::Rect rect;
@ -185,10 +189,10 @@ void ListScreen::_drawItems(Context &ctx) const {
//rect.y2 = listHeight; //rect.y2 = listHeight;
for (int i = 0; (i < _listLength) && (itemY < listHeight); i++) { for (int i = 0; (i < _listLength) && (itemY < listHeight); i++) {
int itemHeight = ctx.font.metrics.lineHeight + LIST_ITEM_PADDING * 2; int itemHeight = lineHeight + LIST_ITEM_PADDING * 2;
if (i == _activeItem) if (i == _activeItem)
itemHeight += ctx.font.metrics.lineHeight; itemHeight += lineHeight;
if ((itemY + itemHeight) >= 0) { if ((itemY + itemHeight) >= 0) {
if (i == _activeItem) { if (i == _activeItem) {
@ -201,15 +205,15 @@ void ListScreen::_drawItems(Context &ctx) const {
itemHeight, ctx.colors[COLOR_HIGHLIGHT1] itemHeight, ctx.colors[COLOR_HIGHLIGHT1]
); );
rect.y1 = itemY + LIST_ITEM_PADDING + ctx.font.metrics.lineHeight; rect.y1 = itemY + LIST_ITEM_PADDING + lineHeight;
rect.y2 = rect.y1 + ctx.font.metrics.lineHeight; rect.y2 = rect.y1 + lineHeight;
ctx.font.draw( ctx.font.draw(
ctx.gpuCtx, _itemPrompt, rect, ctx.colors[COLOR_SUBTITLE] ctx.gpuCtx, _itemPrompt, rect, ctx.colors[COLOR_SUBTITLE]
); );
} }
rect.y1 = itemY + LIST_ITEM_PADDING; rect.y1 = itemY + LIST_ITEM_PADDING;
rect.y2 = rect.y1 + ctx.font.metrics.lineHeight; rect.y2 = rect.y1 + lineHeight;
ctx.font.draw( ctx.font.draw(
ctx.gpuCtx, _getItemName(ctx, i), rect, ctx.colors[COLOR_TITLE] ctx.gpuCtx, _getItemName(ctx, i), rect, ctx.colors[COLOR_TITLE]
); );
@ -231,6 +235,7 @@ void ListScreen::draw(Context &ctx, bool active) const {
int screenWidth = ctx.gpuCtx.width - SCREEN_MARGIN_X * 2; int screenWidth = ctx.gpuCtx.width - SCREEN_MARGIN_X * 2;
int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
int listHeight = _getListHeight(ctx); int listHeight = _getListHeight(ctx);
int lineHeight = ctx.font.getLineHeight();
_newLayer( _newLayer(
ctx, SCREEN_MARGIN_X, SCREEN_MARGIN_Y, screenWidth, screenHeight ctx, SCREEN_MARGIN_X, SCREEN_MARGIN_Y, screenWidth, screenHeight
@ -242,7 +247,7 @@ void ListScreen::draw(Context &ctx, bool active) const {
rect.x1 = 0; rect.x1 = 0;
rect.y1 = 0; rect.y1 = 0;
rect.x2 = screenWidth; rect.x2 = screenWidth;
rect.y2 = ctx.font.metrics.lineHeight; rect.y2 = lineHeight;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);
rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT; rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT;
@ -251,8 +256,8 @@ void ListScreen::draw(Context &ctx, bool active) const {
_newLayer( _newLayer(
ctx, SCREEN_MARGIN_X, ctx, SCREEN_MARGIN_X,
SCREEN_MARGIN_Y + ctx.font.metrics.lineHeight + SCREEN_BLOCK_MARGIN, SCREEN_MARGIN_Y + lineHeight + SCREEN_BLOCK_MARGIN, screenWidth,
screenWidth, listHeight listHeight
); );
_setBlendMode(ctx, GP0_BLEND_SEMITRANS, true); _setBlendMode(ctx, GP0_BLEND_SEMITRANS, true);
@ -270,23 +275,22 @@ void ListScreen::draw(Context &ctx, bool active) const {
// Up/down arrow icons // Up/down arrow icons
gpu::RectWH iconRect; gpu::RectWH iconRect;
char arrow[2]{ 0, 0 };
iconRect.x = screenWidth - iconRect.x = screenWidth - (lineHeight + LIST_BOX_PADDING);
(ctx.font.metrics.lineHeight + LIST_BOX_PADDING); iconRect.w = lineHeight;
iconRect.w = ctx.font.metrics.lineHeight; iconRect.h = lineHeight;
iconRect.h = ctx.font.metrics.lineHeight;
if (_activeItem) { if (_activeItem) {
arrow[0] = CH_UP_ARROW;
iconRect.y = LIST_BOX_PADDING; iconRect.y = LIST_BOX_PADDING;
ctx.font.draw(ctx.gpuCtx, arrow, iconRect, ctx.colors[COLOR_TEXT1]); ctx.font.draw(
ctx.gpuCtx, CH_UP_ARROW, iconRect, ctx.colors[COLOR_TEXT1]
);
} }
if (_activeItem < (_listLength - 1)) { if (_activeItem < (_listLength - 1)) {
arrow[0] = CH_DOWN_ARROW; iconRect.y = listHeight - (lineHeight + LIST_BOX_PADDING);
iconRect.y = listHeight - ctx.font.draw(
(ctx.font.metrics.lineHeight + LIST_BOX_PADDING); ctx.gpuCtx, CH_DOWN_ARROW, iconRect, ctx.colors[COLOR_TEXT1]
ctx.font.draw(ctx.gpuCtx, arrow, iconRect, ctx.colors[COLOR_TEXT1]); );
} }
} }
} }
@ -323,8 +327,9 @@ void ListScreen::update(Context &ctx) {
} }
// Scroll the list if the selected item is not fully visible. // Scroll the list if the selected item is not fully visible.
int itemHeight = ctx.font.metrics.lineHeight + LIST_ITEM_PADDING * 2; int lineHeight = ctx.font.getLineHeight();
int activeItemHeight = itemHeight + ctx.font.metrics.lineHeight; int itemHeight = lineHeight + LIST_ITEM_PADDING * 2;
int activeItemHeight = lineHeight + itemHeight;
int topOffset = _activeItem * itemHeight; int topOffset = _activeItem * itemHeight;
int bottomOffset = topOffset + activeItemHeight - _getListHeight(ctx); int bottomOffset = topOffset + activeItemHeight - _getListHeight(ctx);

View File

@ -70,7 +70,7 @@ private:
inline int _getListHeight(Context &ctx) const { inline int _getListHeight(Context &ctx) const {
int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2; int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
return screenHeight - ( return screenHeight - (
ctx.font.metrics.lineHeight + SCREEN_PROMPT_HEIGHT + ctx.font.getLineHeight() + SCREEN_PROMPT_HEIGHT +
SCREEN_BLOCK_MARGIN * 2 SCREEN_BLOCK_MARGIN * 2
); );
} }

View File

@ -51,7 +51,7 @@ void MessageBoxScreen::draw(Context &ctx, bool active) const {
rect.y = buttonY + BUTTON_PADDING; rect.y = buttonY + BUTTON_PADDING;
rect.w = _getButtonWidth(); rect.w = _getButtonWidth();
rect.h = rect.y + ctx.font.metrics.lineHeight; rect.h = rect.y + ctx.font.getLineHeight();
//rect.h = BUTTON_HEIGHT - BUTTON_PADDING * 2; //rect.h = BUTTON_HEIGHT - BUTTON_PADDING * 2;
for (int i = 0; i < _numButtons; i++) { for (int i = 0; i < _numButtons; i++) {
@ -184,7 +184,7 @@ void HexEntryScreen::draw(Context &ctx, bool active) const {
rect.x1 = stringOffset; rect.x1 = stringOffset;
rect.y1 = boxY + BUTTON_PADDING; rect.y1 = boxY + BUTTON_PADDING;
rect.x2 = _width - MODAL_PADDING; rect.x2 = _width - MODAL_PADDING;
rect.y2 = boxY + BUTTON_PADDING + ctx.font.metrics.lineHeight; rect.y2 = boxY + BUTTON_PADDING + ctx.font.getLineHeight();
ctx.font.draw(ctx.gpuCtx, string, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, string, rect, ctx.colors[COLOR_TITLE]);
// Highlighted field // Highlighted field
@ -297,7 +297,7 @@ void DateEntryScreen::show(Context &ctx, bool goBack) {
_charWidth = ctx.font.getCharacterWidth('0'); _charWidth = ctx.font.getCharacterWidth('0');
int dateSepWidth = ctx.font.getCharacterWidth('-'); int dateSepWidth = ctx.font.getCharacterWidth('-');
int spaceWidth = ctx.font.metrics.spaceWidth; int spaceWidth = ctx.font.getSpaceWidth();
int timeSepWidth = ctx.font.getCharacterWidth(':'); int timeSepWidth = ctx.font.getCharacterWidth(':');
_fieldOffsets[0] = 0; _fieldOffsets[0] = 0;
@ -354,7 +354,7 @@ void DateEntryScreen::draw(Context &ctx, bool active) const {
rect.x1 = stringOffset; rect.x1 = stringOffset;
rect.y1 = boxY + BUTTON_PADDING; rect.y1 = boxY + BUTTON_PADDING;
rect.x2 = _width - MODAL_PADDING; rect.x2 = _width - MODAL_PADDING;
rect.y2 = boxY + BUTTON_PADDING + ctx.font.metrics.lineHeight; rect.y2 = boxY + BUTTON_PADDING + ctx.font.getLineHeight();
ctx.font.draw(ctx.gpuCtx, string, rect, ctx.colors[COLOR_TITLE]); ctx.font.draw(ctx.gpuCtx, string, rect, ctx.colors[COLOR_TITLE]);
// Highlighted field // Highlighted field

View File

@ -174,7 +174,7 @@ def createParser() -> ArgumentParser:
) )
group.add_argument( group.add_argument(
"configFile", "configFile",
type = FileType("rt"), type = FileType("rt", encoding = "utf-8"),
help = "Path to JSON configuration file", help = "Path to JSON configuration file",
) )
group.add_argument( group.add_argument(

View File

@ -72,7 +72,7 @@ def createParser() -> ArgumentParser:
) )
group.add_argument( group.add_argument(
"configFile", "configFile",
type = FileType("rt"), type = FileType("rt", encoding = "utf-8"),
help = "Path to JSON configuration file", help = "Path to JSON configuration file",
) )
group.add_argument( group.add_argument(
@ -101,7 +101,9 @@ def main():
data: ByteString = bytes(int(asset.get("size", 0))) data: ByteString = bytes(int(asset.get("size", 0)))
case "text": case "text":
with open(sourceDir / asset["source"], "rt") as file: with open(
sourceDir / asset["source"], "rt", encoding = "utf-8"
) as file:
data: ByteString = file.read().encode("ascii") data: ByteString = file.read().encode("ascii")
case "binary": case "binary":
@ -128,7 +130,10 @@ def main():
if "metrics" in asset: if "metrics" in asset:
metrics: dict = asset["metrics"] metrics: dict = asset["metrics"]
else: else:
with open(sourceDir / asset["source"], "rt") as file: with open(
sourceDir / asset["source"], "rt",
encoding = "utf-8"
) as file:
metrics: dict = json.load(file) metrics: dict = json.load(file)
data: ByteString = generateFontMetrics(metrics) data: ByteString = generateFontMetrics(metrics)
@ -137,7 +142,10 @@ def main():
if "palette" in asset: if "palette" in asset:
palette: dict = asset["palette"] palette: dict = asset["palette"]
else: else:
with open(sourceDir / asset["source"], "rt") as file: with open(
sourceDir / asset["source"], "rt",
encoding = "utf-8"
) as file:
palette: dict = json.load(file) palette: dict = json.load(file)
data: ByteString = generateColorPalette(palette) data: ByteString = generateColorPalette(palette)
@ -146,7 +154,10 @@ def main():
if "strings" in asset: if "strings" in asset:
strings: dict = asset["strings"] strings: dict = asset["strings"]
else: else:
with open(sourceDir / asset["source"], "rt") as file: with open(
sourceDir / asset["source"], "rt",
encoding = "utf-8"
) as file:
strings: dict = json.load(file) strings: dict = json.load(file)
data: ByteString = generateStringTable(strings) data: ByteString = generateStringTable(strings)
@ -155,7 +166,10 @@ def main():
if "db" in asset: if "db" in asset:
db: dict = asset["db"] db: dict = asset["db"]
else: else:
with open(sourceDir / asset["source"], "rt") as file: with open(
sourceDir / asset["source"], "rt",
encoding = "utf-8"
) as file:
db: dict = json.load(file) db: dict = json.load(file)
# TODO: implement # TODO: implement

View File

@ -14,16 +14,14 @@
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# 573in1. If not, see <https://www.gnu.org/licenses/>. # 573in1. If not, see <https://www.gnu.org/licenses/>.
import re from itertools import chain
from collections import defaultdict from struct import Struct
from itertools import chain from typing import Any, Generator, Mapping, Sequence
from struct import Struct
from typing import Any, Generator, Mapping, Sequence
import numpy import numpy
from numpy import ndarray from numpy import ndarray
from PIL import Image from PIL import Image
from .util import colorFromString, hashData from .util import colorFromString, generateHashTable, hashData
## .TIM image converter ## .TIM image converter
@ -102,51 +100,49 @@ def generateIndexedTIM(
if (cx < 0) or (cx > 1023) or (cy < 0) or (cy > 1023): if (cx < 0) or (cx > 1023) or (cy < 0) or (cy > 1023):
raise ValueError("palette X/Y coordinates must be in 0-1023 range") raise ValueError("palette X/Y coordinates must be in 0-1023 range")
image, clut = convertIndexedImage(imageObj) image, clut = convertIndexedImage(imageObj)
data: bytearray = bytearray()
mode: int = 0x8 if (clut.size <= 16) else 0x9 data += _TIM_HEADER_STRUCT.pack(
data: bytearray = bytearray( _TIM_HEADER_VERSION,
_TIM_HEADER_STRUCT.pack(_TIM_HEADER_VERSION, mode) 0x8 if (clut.size <= 16) else 0x9
) )
data.extend(_TIM_SECTION_STRUCT.pack( data += _TIM_SECTION_STRUCT.pack(
_TIM_SECTION_STRUCT.size + clut.size * 2, _TIM_SECTION_STRUCT.size + clut.size * 2,
cx, cy, clut.shape[1], clut.shape[0] cx,
)) cy,
clut.shape[1],
clut.shape[0]
)
data.extend(clut) data.extend(clut)
data.extend(_TIM_SECTION_STRUCT.pack( data += _TIM_SECTION_STRUCT.pack(
_TIM_SECTION_STRUCT.size + image.size, _TIM_SECTION_STRUCT.size + image.size,
ix, iy, image.shape[1] // 2, image.shape[0] ix,
)) iy,
image.shape[1] // 2,
image.shape[0]
)
data.extend(image) data.extend(image)
return data return data
## Font metrics generator ## Font metrics generator
_METRICS_HEADER_STRUCT: Struct = Struct("< 3B x") _METRICS_HEADER_STRUCT: Struct = Struct("< 3B b")
_METRICS_ENTRY_STRUCT: Struct = Struct("< 2B H") _METRICS_ENTRY_STRUCT: Struct = Struct("< 2I")
_METRICS_BUCKET_COUNT: int = 256
def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray: def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
data: bytearray = bytearray( spaceWidth: int = int(metrics["spaceWidth"])
_METRICS_HEADER_STRUCT.size + _METRICS_ENTRY_STRUCT.size * 256 tabWidth: int = int(metrics["tabWidth"])
) lineHeight: int = int(metrics["lineHeight"])
baselineOffset: int = int(metrics["baselineOffset"])
spaceWidth: int = int(metrics["spaceWidth"]) entries: dict[int, int] = {}
tabWidth: int = int(metrics["tabWidth"])
lineHeight: int = int(metrics["lineHeight"])
data[0:_METRICS_HEADER_STRUCT.size] = \
_METRICS_HEADER_STRUCT.pack(spaceWidth, tabWidth, lineHeight)
for ch, entry in metrics["characterSizes"].items(): for ch, entry in metrics["characterSizes"].items():
index: int = ord(ch)
#index: int = ch.encode("ascii")[0]
if (index < 0) or (index > 255):
raise ValueError(f"extended character {index} is not supported")
x: int = int(entry["x"]) x: int = int(entry["x"])
y: int = int(entry["y"]) y: int = int(entry["y"])
w: int = int(entry["width"]) w: int = int(entry["width"])
@ -160,12 +156,38 @@ def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
if h > lineHeight: if h > lineHeight:
raise ValueError("character height exceeds line height") raise ValueError("character height exceeds line height")
offset: int = \ entries[ord(ch)] = (0
_METRICS_HEADER_STRUCT.size + _METRICS_ENTRY_STRUCT.size * index | (x << 0)
data[offset:offset + _METRICS_ENTRY_STRUCT.size] = \ | (y << 8)
_METRICS_ENTRY_STRUCT.pack(x, y, w | (h << 7) | (i << 14)) | (w << 16)
| (h << 23)
| (i << 30)
)
return data buckets, chained = generateHashTable(entries, _METRICS_BUCKET_COUNT)
table: bytearray = bytearray()
if (len(buckets) + len(chained)) > 2048:
raise RuntimeError("font hash table must have <=2048 entries")
table += _METRICS_HEADER_STRUCT.pack(
spaceWidth,
tabWidth,
lineHeight,
baselineOffset
)
for entry in chain(buckets, chained):
if entry is None:
table += _METRICS_ENTRY_STRUCT.pack(0, 0)
continue
table += _METRICS_ENTRY_STRUCT.pack(
entry.fullHash | (entry.chainIndex << 21),
entry.data
)
return table
## Color palette generator ## Color palette generator
@ -207,125 +229,71 @@ def generateColorPalette(
else: else:
r, g, b = color r, g, b = color
data.extend(_PALETTE_ENTRY_STRUCT.pack(r, g, b)) data += _PALETTE_ENTRY_STRUCT.pack(r, g, b)
return data return data
## String table generator ## String table generator
_TABLE_ENTRY_STRUCT: Struct = Struct("< I 2H") _STRING_TABLE_ENTRY_STRUCT: Struct = Struct("< I 2H")
_TABLE_BUCKET_COUNT: int = 256 _STRING_TABLE_BUCKET_COUNT: int = 256
_TABLE_STRING_ALIGN: int = 4 _STRING_TABLE_ALIGNMENT: int = 4
_TABLE_ESCAPE_REGEX: re.Pattern = re.compile(rb"\$?\{(.+?)\}")
_TABLE_ESCAPE_REPL: Mapping[bytes, bytes] = {
b"UP_ARROW": b"\x80",
b"DOWN_ARROW": b"\x81",
b"LEFT_ARROW": b"\x82",
b"RIGHT_ARROW": b"\x83",
b"UP_ARROW_ALT": b"\x84",
b"DOWN_ARROW_ALT": b"\x85",
b"LEFT_ARROW_ALT": b"\x86",
b"RIGHT_ARROW_ALT": b"\x87",
b"LEFT_BUTTON": b"\x90",
b"RIGHT_BUTTON": b"\x91",
b"START_BUTTON": b"\x92",
b"CLOSED_LOCK": b"\x93",
b"OPEN_LOCK": b"\x94",
b"CHIP_ICON": b"\x95",
b"CART_ICON": b"\x96",
b"CDROM_ICON": b"\xa0",
b"HDD_ICON": b"\xa1",
b"HOST_ICON": b"\xa2",
b"DIR_ICON": b"\xa3",
b"PARENT_DIR_ICON": b"\xa4",
b"FILE_ICON": b"\xa5"
}
def _convertString(string: str) -> bytes:
return _TABLE_ESCAPE_REGEX.sub(
lambda match: _TABLE_ESCAPE_REPL[match.group(1).strip().upper()],
string.encode("ascii")
)
def _walkStringTree( def _walkStringTree(
strings: Mapping[str, Any], prefix: str = "" strings: Mapping[str, Any], prefix: str = ""
) -> Generator[tuple[int, bytes | None], None, None]: ) -> Generator[tuple[int, bytes | None], None, None]:
for key, value in strings.items(): for key, value in strings.items():
fullKey: str = prefix + key fullKey: str = prefix + key
keyHash: int = hashData(fullKey.encode("ascii"))
if value is None: if value is None:
yield hashData(fullKey.encode("ascii")), None yield keyHash, None
elif isinstance(value, str): elif isinstance(value, str):
yield hashData(fullKey.encode("ascii")), _convertString(value) yield keyHash, value.encode("utf-8")
else: else:
yield from _walkStringTree(value, f"{fullKey}.") yield from _walkStringTree(value, f"{fullKey}.")
def generateStringTable(strings: Mapping[str, Any]) -> bytearray: def generateStringTable(strings: Mapping[str, Any]) -> bytearray:
offsets: dict[bytes, int] = {} offsets: dict[bytes, int] = {}
chains: defaultdict[int, list[tuple[int, int | None]]] = defaultdict(list) entries: dict[int, int] = {}
blob: bytearray = bytearray()
blob: bytearray = bytearray() for keyHash, string in _walkStringTree(strings):
for fullHash, string in _walkStringTree(strings):
if string is None: if string is None:
entry: tuple[int, int | None] = fullHash, 0 entries[keyHash] = 0
else:
offset: int | None = offsets.get(string, None)
if offset is None:
offset = len(blob)
offsets[string] = offset
blob.extend(string)
blob.append(0)
while len(blob) % _TABLE_STRING_ALIGN:
blob.append(0)
entry: tuple[int, int | None] = fullHash, offset
chains[fullHash % _TABLE_BUCKET_COUNT].append(entry)
# Build the bucket array and all chains of entries.
buckets: list[tuple[int, int | None, int]] = []
chained: list[tuple[int, int | None, int]] = []
for shortHash in range(_TABLE_BUCKET_COUNT):
entries: list[tuple[int, int | None]] = chains[shortHash]
if not entries:
buckets.append(( 0, None, 0 ))
continue continue
for index, entry in enumerate(entries): # Identical strings associated to multiple keys are deduplicated.
if index < (len(entries) - 1): offset: int | None = offsets.get(string, None)
chainIndex: int = _TABLE_BUCKET_COUNT + len(chained)
else:
chainIndex: int = 0
fullHash, offset = entry if offset is None:
offset = len(blob)
offsets[string] = offset
if index: blob += string
chained.append(( fullHash, offset, chainIndex + 1 )) blob.append(0)
else:
buckets.append(( fullHash, offset, chainIndex )) while len(blob) % _STRING_TABLE_ALIGNMENT:
blob.append(0)
entries[keyHash] = offset
buckets, chained = generateHashTable(entries, _STRING_TABLE_BUCKET_COUNT)
table: bytearray = bytearray()
# Relocate the offsets and serialize the table. # Relocate the offsets and serialize the table.
totalLength: int = len(buckets) + len(chained) blobOffset: int = \
blobOffset: int = _TABLE_ENTRY_STRUCT.size * totalLength (len(buckets) + len(chained)) * _STRING_TABLE_ENTRY_STRUCT.size
data: bytearray = bytearray()
for fullHash, offset, chainIndex in chain(buckets, chained): for entry in chain(buckets, chained):
absOffset: int = 0 if (offset is None) else (blobOffset + offset) if entry is None:
table += _STRING_TABLE_ENTRY_STRUCT.pack(0, 0, 0)
continue
if absOffset > 0xffff: table += _STRING_TABLE_ENTRY_STRUCT.pack(
raise RuntimeError("string table exceeds 64 KB size limit") entry.fullHash,
0 if (entry.data is None) else (blobOffset + entry.data),
entry.chainIndex
)
data.extend(_TABLE_ENTRY_STRUCT.pack(fullHash, absOffset, chainIndex)) return table + blob
data.extend(blob)
return data

View File

@ -15,10 +15,13 @@
# 573in1. If not, see <https://www.gnu.org/licenses/>. # 573in1. If not, see <https://www.gnu.org/licenses/>.
import logging, re import logging, re
from hashlib import md5 from collections import defaultdict
from io import SEEK_END, SEEK_SET from dataclasses import dataclass
from typing import \ from hashlib import md5
Any, BinaryIO, ByteString, Iterable, Iterator, Sequence, TextIO from io import SEEK_END, SEEK_SET
from typing import \
Any, BinaryIO, ByteString, Generator, Iterable, Iterator, Mapping, \
Sequence, TextIO
## Value manipulation ## Value manipulation
@ -142,6 +145,44 @@ def setupLogger(level: int | None):
)[min(level or 0, 2)] )[min(level or 0, 2)]
) )
## Hash table generator
@dataclass
class HashTableEntry:
fullHash: int
chainIndex: int
data: Any
def generateHashTable(
entries: Mapping[int, Any], numBuckets: int
) -> tuple[list[HashTableEntry | None], list[HashTableEntry]]:
chains: defaultdict[int, list[HashTableEntry]] = defaultdict(list)
for fullHash, data in entries.items():
entry: HashTableEntry = HashTableEntry(fullHash, 0, data)
chains[fullHash % numBuckets].append(entry)
buckets: list[HashTableEntry | None] = []
chained: list[HashTableEntry] = []
for shortHash in range(numBuckets):
entries: list[HashTableEntry] = chains[shortHash]
if not len(entries): # Empty bucket
buckets.append(None)
continue
for index, entry in enumerate(entries):
entry.chainIndex = numBuckets + len(chained) + index
entries[-1].chainIndex = 0 # Terminate chain
buckets.append(entries[0])
chained += entries[1:]
return buckets, chained
## Odd/even interleaved file reader ## Odd/even interleaved file reader
class InterleavedFile(BinaryIO): class InterleavedFile(BinaryIO):

View File

@ -82,7 +82,7 @@ def createParser() -> ArgumentParser:
) )
group.add_argument( group.add_argument(
"-l", "--log", "-l", "--log",
type = FileType("at"), type = FileType("at", encoding = "utf-8"),
default = sys.stdout, default = sys.stdout,
help = "Log cartridge info to specified file (stdout by default)", help = "Log cartridge info to specified file (stdout by default)",
metavar = "file" metavar = "file"