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

View File

@ -1,7 +1,7 @@
{
"AboutScreen": {
"title": "{RIGHT_ARROW} About 573in1",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back."
"title": " About 573in1",
"prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back."
},
"App": {
@ -73,7 +73,7 @@
"erase": "Erasing existing 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.",
"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": {
"compress": "Compressing cartridge dump...",
@ -122,9 +122,9 @@
},
"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.",
"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",
"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.",
"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": {
"title": "{RIGHT_ARROW} 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.",
"itemPrompt": "{RIGHT_ARROW} Press and hold {START_BUTTON} or Test to confirm",
"title": " Select button mapping",
"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": "▸ Press and hold ▭ or Test to confirm",
"joystick": "JAMMA supergun or joystick/buttons",
"ddrCab": "Dance Dance Revolution (2-player) cabinet",
@ -166,8 +166,8 @@
},
"CartActionsScreen": {
"title": "{RIGHT_ARROW} Cartridge options",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back",
"title": " Cartridge options",
"itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"qrDump": {
"name": "Dump cartridge as QR code",
@ -218,7 +218,7 @@
},
"CartInfoScreen": {
"title": "{RIGHT_ARROW} Cartridge information",
"title": " Cartridge information",
"digitalIO": {
"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"
},
"unlockStatus": {
"locked": "{CLOSED_LOCK} locked, game key required",
"unlocked": "{OPEN_LOCK} unlocked"
"locked": "🔒 locked, game key required",
"unlocked": "🔓 unlocked"
},
"id": {
"error": "read failure",
@ -265,9 +265,9 @@
}
},
"prompt": {
"locked": "{RIGHT_ARROW} Press {START_BUTTON} to unlock, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.",
"unlocked": "{RIGHT_ARROW} Press {START_BUTTON} to continue, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.",
"error": "{RIGHT_ARROW} Hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back."
"locked": "▸ Press ▭ to unlock, hold ◁▷ + ▭ to go back.",
"unlocked": "▸ Press ▭ to continue, hold ◁▷ + ▭ to go back.",
"error": "▸ Hold ◁▷ + ▭ to go back."
},
"unlockWarning": {
@ -278,8 +278,8 @@
},
"ChecksumScreen": {
"title": "{RIGHT_ARROW} Storage device checksums",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back.",
"title": " Storage device checksums",
"prompt": "▸ Press ▭ to go back.",
"bios": "BIOS ROM (512 KB):\t\t\t\t%08X\n",
"rtc": "RTC RAM (8184 bytes):\t\t\t%08X\n",
@ -289,8 +289,8 @@
},
"ColorIntensityScreen": {
"title": "{RIGHT_ARROW} Monitor color intensity test",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back.",
"title": " Monitor color intensity test",
"prompt": "▸ Press ▭ to go back.",
"white": "White",
"red": "Red",
@ -305,18 +305,18 @@
},
"FileBrowserScreen": {
"title": "{RIGHT_ARROW} Select file",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back",
"title": " Select file",
"itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"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."
},
"FilePickerScreen": {
"title": "{RIGHT_ARROW} Select IDE drive",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back",
"title": " Select IDE drive",
"itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"host": "{HOST_ICON} Host filesystem (PCDRV)",
"host": "🖧 Host filesystem (PCDRV)",
"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.",
@ -326,18 +326,18 @@
},
"GeometryScreen": {
"title": "{RIGHT_ARROW} Monitor geometry test",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} to go back."
"title": " Monitor geometry test",
"prompt": "▸ Press ▭ to go back."
},
"HexdumpScreen": {
"title": "{RIGHT_ARROW} Cartridge dump",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back."
"title": " Cartridge dump",
"prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back."
},
"IDEInfoScreen": {
"title": "{RIGHT_ARROW} IDE device information",
"prompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to scroll. Press {START_BUTTON} to go back.",
"title": " IDE device information",
"prompt": "▸ Use ◁▷ to scroll. Press ▭ to go back.",
"device": {
"header": {
@ -371,8 +371,8 @@
},
"JAMMATestScreen": {
"title": "{RIGHT_ARROW} JAMMA input test",
"prompt": "{RIGHT_ARROW} Press and hold {START_BUTTON} to go back.",
"title": " JAMMA input test",
"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",
"inputs": "The following buttons are currently held down:\n",
@ -389,7 +389,7 @@
"button4": " Player 1 button 4\t\tJAMMA pin 25\n",
"button5": " Player 1 button 5\t\tJAMMA pin 26\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": {
"left": " Player 2 joystick left\t\tJAMMA pin X\n",
@ -402,7 +402,7 @@
"button4": " Player 2 button 4\t\tJAMMA pin c\n",
"button5": " Player 2 button 5\t\tJAMMA pin d\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",
@ -413,14 +413,14 @@
"KeyEntryScreen": {
"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",
"ok": "Confirm"
},
"MainMenuScreen": {
"title": "{RIGHT_ARROW} 573in1",
"itemPrompt": "{RIGHT_ARROW} Use {LEFT_BUTTON}{RIGHT_BUTTON} to move, select by pressing {START_BUTTON}",
"title": " 573in1",
"itemPrompt": "▸ Use ◁▷ to move, select by pressing ▭",
"cartInfo": {
"name": "Manage security cartridge",
@ -476,20 +476,20 @@
},
"QRCodeScreen": {
"title": "{RIGHT_ARROW} 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."
"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 to go back."
},
"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.",
"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": {
"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.",
"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",
"320x240i": "320x240 (4:3), interlaced (line doubled)",
@ -504,14 +504,14 @@
"RTCTimeScreen": {
"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",
"ok": "Confirm"
},
"StorageActionsScreen": {
"title": "{RIGHT_ARROW} Storage device options",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back",
"title": " Storage device options",
"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.",
"runExecutable": {
@ -616,8 +616,8 @@
},
"StorageInfoScreen": {
"title": "{RIGHT_ARROW} Storage device information",
"prompt": "{RIGHT_ARROW} Press {START_BUTTON} for more options, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back.",
"title": " Storage device information",
"prompt": "▸ Press ▭ for more options, hold ◁▷ + ▭ to go back.",
"bios": {
"header": "BIOS ROM:\n",
@ -666,14 +666,14 @@
"SystemIDEntryScreen": {
"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",
"ok": "Confirm"
},
"TestMenuScreen": {
"title": "{RIGHT_ARROW} Hardware test suite",
"itemPrompt": "{RIGHT_ARROW} Press {START_BUTTON} to select, hold {LEFT_BUTTON}{RIGHT_BUTTON} + {START_BUTTON} to go back",
"title": " Hardware test suite",
"itemPrompt": "▸ Press ▭ to select, hold ◁▷ + ▭ to go back",
"jammaTest": {
"name": "Test JAMMA inputs",
@ -694,9 +694,9 @@
},
"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.",
"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)",
"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.",
"cooldown": "Wait... (%ds)",
"ok": "{START_BUTTON} Continue"
"ok": " Continue"
},
"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"
},
{
"type": "strings",
"name": "assets/lang/en.lang",
"source": "${PROJECT_SOURCE_DIR}/assets/lang/en.json"
},
{
"type": "tim",
"name": "assets/textures/background.tim",
@ -32,21 +37,21 @@
"name": "assets/textures/font.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/font.png",
"quantize": 16,
"imagePos": { "x": 984, "y": 0 },
"clutPos": { "x": 1008, "y": 1 }
"imagePos": { "x": 960, "y": 126 },
"clutPos": { "x": 1008, "y": 1 }
},
{
"type": "tim",
"name": "assets/textures/splash.tim",
"source": "${PROJECT_SOURCE_DIR}/assets/textures/splash.png",
"quantize": 16,
"imagePos": { "x": 960, "y": 96 },
"clutPos": { "x": 1008, "y": 2 }
"imagePos": { "x": 984, "y": 0 },
"clutPos": { "x": 1008, "y": 2 }
},
{
"type": "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",
@ -96,21 +101,17 @@
"source": "${PROJECT_SOURCE_DIR}/assets/sounds/screenshot.vag",
"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",
"name": "assets/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",

View File

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

View File

@ -5,7 +5,13 @@
"title": "Root",
"type": "object",
"required": [ "spaceWidth", "tabWidth", "lineHeight", "characterSizes" ],
"required": [
"spaceWidth",
"tabWidth",
"lineHeight",
"baselineOffset",
"characterSizes"
],
"properties": {
"spaceWidth": {
@ -20,13 +26,18 @@
},
"lineHeight": {
"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"
},
"characterSizes": {
"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",
"additionalProperties": false,

View File

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

View File

@ -30,28 +30,28 @@
#define VERSION_STRING VERSION "-debug"
#endif
enum Character : char {
CH_UP_ARROW = '\x80',
CH_DOWN_ARROW = '\x81',
CH_LEFT_ARROW = '\x82',
CH_RIGHT_ARROW = '\x83',
CH_UP_ARROW_ALT = '\x84',
CH_DOWN_ARROW_ALT = '\x85',
CH_LEFT_ARROW_ALT = '\x86',
CH_RIGHT_ARROW_ALT = '\x87',
#define CH_UP_ARROW "\u25b4"
#define CH_DOWN_ARROW "\u25be"
#define CH_LEFT_ARROW "\u25c2"
#define CH_RIGHT_ARROW "\u25b8"
#define CH_UP_ARROW_ALT "\u2191"
#define CH_DOWN_ARROW_ALT "\u2193"
#define CH_LEFT_ARROW_ALT "\u2190"
#define CH_RIGHT_ARROW_ALT "\u2192"
#define CH_INVALID_CHAR "\ufffd"
CH_LEFT_BUTTON = '\x90',
CH_RIGHT_BUTTON = '\x91',
CH_START_BUTTON = '\x92',
CH_CLOSED_LOCK = '\x93',
CH_OPEN_LOCK = '\x94',
CH_CHIP_ICON = '\x95',
CH_CART_ICON = '\x96',
CH_CDROM_ICON = '\xa0',
CH_HDD_ICON = '\xa1',
CH_HOST_ICON = '\xa2',
CH_DIR_ICON = '\xa3',
CH_PARENT_DIR_ICON = '\xa4',
CH_FILE_ICON = '\xa5'
};
#define CH_LEFT_BUTTON "\u25c1"
#define CH_RIGHT_BUTTON "\u25b7"
#define CH_START_BUTTON "\u25ad"
#define CH_CLOSED_LOCK "\U0001f512"
#define CH_OPEN_LOCK "\U0001f513"
#define CH_CIRCLE_BUTTON "\u25cb"
#define CH_X_BUTTON "\u2715"
#define CH_TRIANGLE_BUTTON "\u25b3"
#define CH_SQUARE_BUTTON "\u25a1"
#define CH_CDROM_ICON "\U0001f5b8"
#define CH_HDD_ICON "\U0001f5b4"
#define CH_HOST_ICON "\U0001f5a7"
#define CH_DIR_ICON "\U0001f5c0"
#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)
return _ERROR_STRING;
auto blob = reinterpret_cast<const char *>(ptr);
auto table = reinterpret_cast<const StringTableEntry *>(ptr);
auto blob = as<const char>();
auto table = as<const StringTableEntry>();
auto index = id % STRING_TABLE_BUCKET_COUNT;
auto entry = &table[id % TABLE_BUCKET_COUNT];
if (entry->hash == id)
return &blob[entry->offset];
while (entry->chained) {
entry = &table[entry->chained];
do {
auto entry = &table[index];
index = entry->chained;
if (entry->hash == id)
return &blob[entry->offset];
}
} while (index);
return _ERROR_STRING;
}

View File

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

View File

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

View File

@ -16,34 +16,75 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "common/gpufont.hpp"
#include "common/util/string.hpp"
#include "common/util/templates.hpp"
#include "common/gpu.hpp"
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:
uint8_t spaceWidth, tabWidth, lineHeight, _reserved;
uint32_t characterSizes[256];
uint8_t spaceWidth, tabWidth, lineHeight;
int8_t baselineOffset;
};
inline uint32_t getCharacterSize(uint8_t ch) const {
uint32_t sizes = characterSizes[ch];
if (!sizes)
return characterSizes[int(FONT_INVALID_CHAR)];
struct FontMetricsEntry {
public:
uint32_t codePoint;
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 {
public:
Image image;
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(
Context &ctx, const char *str, const Rect &rect, const Rect &clipRect,
Color color = 0x808080, bool wordWrap = false
@ -56,7 +97,7 @@ public:
Context &ctx, const char *str, const RectWH &rect,
Color color = 0x808080, bool wordWrap = false
) const;
int getCharacterWidth(char ch) const;
int getCharacterWidth(util::UTF8CodePoint ch) const;
void getStringBounds(
const char *str, Rect &rect, bool wordWrap = false,
bool breakOnSpace = false

View File

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

View File

@ -110,6 +110,24 @@ size_t encodeBase41(char *output, const uint8_t *input, size_t length) {
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 */
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 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 */
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) {
auto &res = _fileIO.resource;
res.loadStruct(_ctx.colors, "assets/palette.dat");
res.loadTIM(_background.tile, "assets/textures/background.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.loadStruct(_ctx.colors, "assets/app.palette");
res.loadData(_stringTable, "assets/app.strings");
res.loadData(_stringTable, "assets/lang/en.lang");
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 fs = APP->_fileIO.ide[drive];
auto icon = (dev.flags & ide::DEVICE_ATAPI)
? CH_CDROM_ICON
: CH_HDD_ICON;
auto label = fs
auto format = (dev.flags & ide::DEVICE_ATAPI)
? (CH_CDROM_ICON " %s: %s")
: (CH_HDD_ICON " %s: %s");
auto label = fs
? fs->volumeLabel
: STR("FilePickerScreen.noFS");
snprintf(name, sizeof(name), "%c %s: %s", icon, dev.model, label);
snprintf(name, sizeof(name), format, dev.model, label);
return name;
}
@ -310,26 +310,24 @@ const char *FileBrowserScreen::_getItemName(ui::Context &ctx, int index) const {
if (!_isRoot)
index--;
const char *path;
const char *format, *path;
if (index < 0) {
name[0] = CH_PARENT_DIR_ICON;
path = STR("FileBrowserScreen.parentDir");
format = CH_PARENT_DIR_ICON " %s";
path = STR("FileBrowserScreen.parentDir");
} else if (index < _numDirectories) {
auto entries = _directories.as<file::FileInfo>();
name[0] = CH_DIR_ICON;
path = entries[index].name;
format = CH_DIR_ICON " %s";
path = entries[index].name;
} else {
auto entries = _files.as<file::FileInfo>();
name[0] = CH_FILE_ICON;
path = entries[index - _numDirectories].name;
format = CH_FILE_ICON " %s";
path = entries[index - _numDirectories].name;
}
name[1] = ' ';
__builtin_strncpy(&name[2], path, sizeof(name) - 2);
snprintf(name, sizeof(name), format, path);
return name;
}

View File

@ -275,6 +275,7 @@ void TestPatternScreen::_drawTextOverlay(
) const {
int screenWidth = ctx.gpuCtx.width - ui::SCREEN_MARGIN_X * 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);
@ -283,7 +284,7 @@ void TestPatternScreen::_drawTextOverlay(
backdropRect.x = ui::SCREEN_MARGIN_X - ui::SHADOW_OFFSET;
backdropRect.y = ui::SCREEN_MARGIN_Y - ui::SHADOW_OFFSET;
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);
backdropRect.y += screenHeight - ui::SCREEN_PROMPT_HEIGHT_MIN;
@ -294,7 +295,7 @@ void TestPatternScreen::_drawTextOverlay(
textRect.x1 = ui::SCREEN_MARGIN_X;
textRect.y1 = ui::SCREEN_MARGIN_Y;
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]);
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 {
TestPatternScreen::draw(ctx, active);
int barWidth = _INTENSITY_BAR_NAME_WIDTH + _INTENSITY_BAR_WIDTH;
int barHeight = _INTENSITY_BAR_HEIGHT * util::countOf(_INTENSITY_BARS);
int offsetX = (ctx.gpuCtx.width - barWidth) / 2;
int offsetY = (ctx.gpuCtx.height - barHeight) / 2;
int barWidth = _INTENSITY_BAR_NAME_WIDTH + _INTENSITY_BAR_WIDTH;
int barHeight = _INTENSITY_BAR_HEIGHT * util::countOf(_INTENSITY_BARS);
int offsetX = (ctx.gpuCtx.width - barWidth) / 2;
int offsetY = (ctx.gpuCtx.height - barHeight) / 2;
int lineHeight = ctx.font.getLineHeight();
gpu::RectWH textRect, barRect;
textRect.x = offsetX;
textRect.y =
offsetY + (_INTENSITY_BAR_HEIGHT - ctx.font.metrics.lineHeight) / 2;
offsetY + (_INTENSITY_BAR_HEIGHT - lineHeight) / 2;
textRect.w = _INTENSITY_BAR_NAME_WIDTH;
textRect.h = ctx.font.metrics.lineHeight;
textRect.h = lineHeight;
barRect.x = offsetX + _INTENSITY_BAR_NAME_WIDTH;
barRect.y = offsetY;
@ -381,7 +383,7 @@ void ColorIntensityScreen::draw(ui::Context &ctx, bool active) const {
char value[2]{ 0, 0 };
textRect.x = barRect.x + 1;
textRect.y = offsetY - ctx.font.metrics.lineHeight;
textRect.y = offsetY - lineHeight;
textRect.w = _INTENSITY_BAR_WIDTH / 32;
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 {
int lineHeight = ctx.font.getLineHeight();
gpu::RectWH rect;
rect.y = ctx.gpuCtx.height - (8 + ctx.font.metrics.lineHeight);
rect.h = ctx.font.metrics.lineHeight;
rect.y = ctx.gpuCtx.height - (8 + lineHeight);
rect.h = lineHeight;
if (leftText) {
rect.x = 8;
@ -342,22 +344,22 @@ void LogOverlay::draw(Context &ctx, bool active) const {
// Text
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;
rect.x1 = SCREEN_MIN_MARGIN_X;
rect.y1 = SCREEN_MIN_MARGIN_Y;
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.gpuCtx, _buffer.getLine(i), rect, ctx.colors[COLOR_TEXT1]
);
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.y1 = 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;
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 {
int screenWidth = ctx.gpuCtx.width - SCREEN_MARGIN_X * 2;
int screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
int lineHeight = ctx.font.getLineHeight();
// Top/bottom text
_newLayer(
@ -49,14 +50,14 @@ void TextScreen::draw(Context &ctx, bool active) const {
rect.x1 = 0;
rect.y1 = 0;
rect.x2 = screenWidth;
rect.y2 = ctx.font.metrics.lineHeight;
rect.y2 = lineHeight;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);
rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT_MIN;
rect.y2 = screenHeight;
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 -
(bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN);
@ -82,7 +83,7 @@ void TextScreen::update(Context &ctx) {
return;
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 -
(bodyOffset + SCREEN_PROMPT_HEIGHT_MIN + SCREEN_BLOCK_MARGIN);
@ -128,6 +129,8 @@ _prompt(nullptr) {}
void ImageScreen::draw(Context &ctx, bool active) const {
_newLayer(ctx, 0, 0, ctx.gpuCtx.width, ctx.gpuCtx.height);
int lineHeight = ctx.font.getLineHeight();
if (_image) {
int x = ctx.gpuCtx.width / 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;
if (_prompt)
y -= (SCREEN_PROMPT_HEIGHT - ctx.font.metrics.lineHeight) / 2;
y -= (SCREEN_PROMPT_HEIGHT - lineHeight) / 2;
// Backdrop
if (_imagePadding) {
@ -162,7 +165,7 @@ void ImageScreen::draw(Context &ctx, bool active) const {
rect.x1 = SCREEN_MARGIN_X;
rect.y1 = SCREEN_MARGIN_Y;
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]);
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 itemWidth = _getItemWidth(ctx);
int listHeight = _getListHeight(ctx);
int lineHeight = ctx.font.getLineHeight();
gpu::Rect rect;
@ -185,10 +189,10 @@ void ListScreen::_drawItems(Context &ctx) const {
//rect.y2 = listHeight;
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)
itemHeight += ctx.font.metrics.lineHeight;
itemHeight += lineHeight;
if ((itemY + itemHeight) >= 0) {
if (i == _activeItem) {
@ -201,15 +205,15 @@ void ListScreen::_drawItems(Context &ctx) const {
itemHeight, ctx.colors[COLOR_HIGHLIGHT1]
);
rect.y1 = itemY + LIST_ITEM_PADDING + ctx.font.metrics.lineHeight;
rect.y2 = rect.y1 + ctx.font.metrics.lineHeight;
rect.y1 = itemY + LIST_ITEM_PADDING + lineHeight;
rect.y2 = rect.y1 + lineHeight;
ctx.font.draw(
ctx.gpuCtx, _itemPrompt, rect, ctx.colors[COLOR_SUBTITLE]
);
}
rect.y1 = itemY + LIST_ITEM_PADDING;
rect.y2 = rect.y1 + ctx.font.metrics.lineHeight;
rect.y2 = rect.y1 + lineHeight;
ctx.font.draw(
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 screenHeight = ctx.gpuCtx.height - SCREEN_MARGIN_Y * 2;
int listHeight = _getListHeight(ctx);
int lineHeight = ctx.font.getLineHeight();
_newLayer(
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.y1 = 0;
rect.x2 = screenWidth;
rect.y2 = ctx.font.metrics.lineHeight;
rect.y2 = lineHeight;
ctx.font.draw(ctx.gpuCtx, _title, rect, ctx.colors[COLOR_TITLE]);
rect.y1 = screenHeight - SCREEN_PROMPT_HEIGHT;
@ -251,8 +256,8 @@ void ListScreen::draw(Context &ctx, bool active) const {
_newLayer(
ctx, SCREEN_MARGIN_X,
SCREEN_MARGIN_Y + ctx.font.metrics.lineHeight + SCREEN_BLOCK_MARGIN,
screenWidth, listHeight
SCREEN_MARGIN_Y + lineHeight + SCREEN_BLOCK_MARGIN, screenWidth,
listHeight
);
_setBlendMode(ctx, GP0_BLEND_SEMITRANS, true);
@ -270,23 +275,22 @@ void ListScreen::draw(Context &ctx, bool active) const {
// Up/down arrow icons
gpu::RectWH iconRect;
char arrow[2]{ 0, 0 };
iconRect.x = screenWidth -
(ctx.font.metrics.lineHeight + LIST_BOX_PADDING);
iconRect.w = ctx.font.metrics.lineHeight;
iconRect.h = ctx.font.metrics.lineHeight;
iconRect.x = screenWidth - (lineHeight + LIST_BOX_PADDING);
iconRect.w = lineHeight;
iconRect.h = lineHeight;
if (_activeItem) {
arrow[0] = CH_UP_ARROW;
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)) {
arrow[0] = CH_DOWN_ARROW;
iconRect.y = listHeight -
(ctx.font.metrics.lineHeight + LIST_BOX_PADDING);
ctx.font.draw(ctx.gpuCtx, arrow, iconRect, ctx.colors[COLOR_TEXT1]);
iconRect.y = listHeight - (lineHeight + LIST_BOX_PADDING);
ctx.font.draw(
ctx.gpuCtx, CH_DOWN_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.
int itemHeight = ctx.font.metrics.lineHeight + LIST_ITEM_PADDING * 2;
int activeItemHeight = itemHeight + ctx.font.metrics.lineHeight;
int lineHeight = ctx.font.getLineHeight();
int itemHeight = lineHeight + LIST_ITEM_PADDING * 2;
int activeItemHeight = lineHeight + itemHeight;
int topOffset = _activeItem * itemHeight;
int bottomOffset = topOffset + activeItemHeight - _getListHeight(ctx);

View File

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

View File

@ -51,7 +51,7 @@ void MessageBoxScreen::draw(Context &ctx, bool active) const {
rect.y = buttonY + BUTTON_PADDING;
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;
for (int i = 0; i < _numButtons; i++) {
@ -184,7 +184,7 @@ void HexEntryScreen::draw(Context &ctx, bool active) const {
rect.x1 = stringOffset;
rect.y1 = boxY + BUTTON_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]);
// Highlighted field
@ -297,7 +297,7 @@ void DateEntryScreen::show(Context &ctx, bool goBack) {
_charWidth = ctx.font.getCharacterWidth('0');
int dateSepWidth = ctx.font.getCharacterWidth('-');
int spaceWidth = ctx.font.metrics.spaceWidth;
int spaceWidth = ctx.font.getSpaceWidth();
int timeSepWidth = ctx.font.getCharacterWidth(':');
_fieldOffsets[0] = 0;
@ -354,7 +354,7 @@ void DateEntryScreen::draw(Context &ctx, bool active) const {
rect.x1 = stringOffset;
rect.y1 = boxY + BUTTON_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]);
// Highlighted field

View File

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

View File

@ -72,7 +72,7 @@ def createParser() -> ArgumentParser:
)
group.add_argument(
"configFile",
type = FileType("rt"),
type = FileType("rt", encoding = "utf-8"),
help = "Path to JSON configuration file",
)
group.add_argument(
@ -101,7 +101,9 @@ def main():
data: ByteString = bytes(int(asset.get("size", 0)))
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")
case "binary":
@ -128,7 +130,10 @@ def main():
if "metrics" in asset:
metrics: dict = asset["metrics"]
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)
data: ByteString = generateFontMetrics(metrics)
@ -137,7 +142,10 @@ def main():
if "palette" in asset:
palette: dict = asset["palette"]
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)
data: ByteString = generateColorPalette(palette)
@ -146,7 +154,10 @@ def main():
if "strings" in asset:
strings: dict = asset["strings"]
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)
data: ByteString = generateStringTable(strings)
@ -155,7 +166,10 @@ def main():
if "db" in asset:
db: dict = asset["db"]
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)
# TODO: implement

View File

@ -14,16 +14,14 @@
# You should have received a copy of the GNU General Public License along with
# 573in1. If not, see <https://www.gnu.org/licenses/>.
import re
from collections import defaultdict
from itertools import chain
from struct import Struct
from typing import Any, Generator, Mapping, Sequence
from itertools import chain
from struct import Struct
from typing import Any, Generator, Mapping, Sequence
import numpy
from numpy import ndarray
from PIL import Image
from .util import colorFromString, hashData
from .util import colorFromString, generateHashTable, hashData
## .TIM image converter
@ -102,51 +100,49 @@ def generateIndexedTIM(
if (cx < 0) or (cx > 1023) or (cy < 0) or (cy > 1023):
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: bytearray = bytearray(
_TIM_HEADER_STRUCT.pack(_TIM_HEADER_VERSION, mode)
data += _TIM_HEADER_STRUCT.pack(
_TIM_HEADER_VERSION,
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,
cx, cy, clut.shape[1], clut.shape[0]
))
cx,
cy,
clut.shape[1],
clut.shape[0]
)
data.extend(clut)
data.extend(_TIM_SECTION_STRUCT.pack(
data += _TIM_SECTION_STRUCT.pack(
_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)
return data
## Font metrics generator
_METRICS_HEADER_STRUCT: Struct = Struct("< 3B x")
_METRICS_ENTRY_STRUCT: Struct = Struct("< 2B H")
_METRICS_HEADER_STRUCT: Struct = Struct("< 3B b")
_METRICS_ENTRY_STRUCT: Struct = Struct("< 2I")
_METRICS_BUCKET_COUNT: int = 256
def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
data: bytearray = bytearray(
_METRICS_HEADER_STRUCT.size + _METRICS_ENTRY_STRUCT.size * 256
)
spaceWidth: int = int(metrics["spaceWidth"])
tabWidth: int = int(metrics["tabWidth"])
lineHeight: int = int(metrics["lineHeight"])
baselineOffset: int = int(metrics["baselineOffset"])
spaceWidth: int = int(metrics["spaceWidth"])
tabWidth: int = int(metrics["tabWidth"])
lineHeight: int = int(metrics["lineHeight"])
data[0:_METRICS_HEADER_STRUCT.size] = \
_METRICS_HEADER_STRUCT.pack(spaceWidth, tabWidth, lineHeight)
entries: dict[int, int] = {}
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"])
y: int = int(entry["y"])
w: int = int(entry["width"])
@ -160,12 +156,38 @@ def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
if h > lineHeight:
raise ValueError("character height exceeds line height")
offset: int = \
_METRICS_HEADER_STRUCT.size + _METRICS_ENTRY_STRUCT.size * index
data[offset:offset + _METRICS_ENTRY_STRUCT.size] = \
_METRICS_ENTRY_STRUCT.pack(x, y, w | (h << 7) | (i << 14))
entries[ord(ch)] = (0
| (x << 0)
| (y << 8)
| (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
@ -207,125 +229,71 @@ def generateColorPalette(
else:
r, g, b = color
data.extend(_PALETTE_ENTRY_STRUCT.pack(r, g, b))
data += _PALETTE_ENTRY_STRUCT.pack(r, g, b)
return data
## String table generator
_TABLE_ENTRY_STRUCT: Struct = Struct("< I 2H")
_TABLE_BUCKET_COUNT: int = 256
_TABLE_STRING_ALIGN: 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")
)
_STRING_TABLE_ENTRY_STRUCT: Struct = Struct("< I 2H")
_STRING_TABLE_BUCKET_COUNT: int = 256
_STRING_TABLE_ALIGNMENT: int = 4
def _walkStringTree(
strings: Mapping[str, Any], prefix: str = ""
) -> Generator[tuple[int, bytes | None], None, None]:
for key, value in strings.items():
fullKey: str = prefix + key
keyHash: int = hashData(fullKey.encode("ascii"))
if value is None:
yield hashData(fullKey.encode("ascii")), None
yield keyHash, None
elif isinstance(value, str):
yield hashData(fullKey.encode("ascii")), _convertString(value)
yield keyHash, value.encode("utf-8")
else:
yield from _walkStringTree(value, f"{fullKey}.")
def generateStringTable(strings: Mapping[str, Any]) -> bytearray:
offsets: dict[bytes, int] = {}
chains: defaultdict[int, list[tuple[int, int | None]]] = defaultdict(list)
offsets: dict[bytes, int] = {}
entries: dict[int, int] = {}
blob: bytearray = bytearray()
blob: bytearray = bytearray()
for fullHash, string in _walkStringTree(strings):
for keyHash, string in _walkStringTree(strings):
if string is None:
entry: tuple[int, int | None] = fullHash, 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 ))
entries[keyHash] = 0
continue
for index, entry in enumerate(entries):
if index < (len(entries) - 1):
chainIndex: int = _TABLE_BUCKET_COUNT + len(chained)
else:
chainIndex: int = 0
# Identical strings associated to multiple keys are deduplicated.
offset: int | None = offsets.get(string, None)
fullHash, offset = entry
if offset is None:
offset = len(blob)
offsets[string] = offset
if index:
chained.append(( fullHash, offset, chainIndex + 1 ))
else:
buckets.append(( fullHash, offset, chainIndex ))
blob += string
blob.append(0)
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.
totalLength: int = len(buckets) + len(chained)
blobOffset: int = _TABLE_ENTRY_STRUCT.size * totalLength
data: bytearray = bytearray()
blobOffset: int = \
(len(buckets) + len(chained)) * _STRING_TABLE_ENTRY_STRUCT.size
for fullHash, offset, chainIndex in chain(buckets, chained):
absOffset: int = 0 if (offset is None) else (blobOffset + offset)
for entry in chain(buckets, chained):
if entry is None:
table += _STRING_TABLE_ENTRY_STRUCT.pack(0, 0, 0)
continue
if absOffset > 0xffff:
raise RuntimeError("string table exceeds 64 KB size limit")
table += _STRING_TABLE_ENTRY_STRUCT.pack(
entry.fullHash,
0 if (entry.data is None) else (blobOffset + entry.data),
entry.chainIndex
)
data.extend(_TABLE_ENTRY_STRUCT.pack(fullHash, absOffset, chainIndex))
data.extend(blob)
return data
return table + blob

View File

@ -15,10 +15,13 @@
# 573in1. If not, see <https://www.gnu.org/licenses/>.
import logging, re
from hashlib import md5
from io import SEEK_END, SEEK_SET
from typing import \
Any, BinaryIO, ByteString, Iterable, Iterator, Sequence, TextIO
from collections import defaultdict
from dataclasses import dataclass
from hashlib import md5
from io import SEEK_END, SEEK_SET
from typing import \
Any, BinaryIO, ByteString, Generator, Iterable, Iterator, Mapping, \
Sequence, TextIO
## Value manipulation
@ -142,6 +145,44 @@ def setupLogger(level: int | None):
)[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
class InterleavedFile(BinaryIO):

View File

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