diff --git a/CMakeLists.txt b/CMakeLists.txt index ae7ea6d..829cc79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/assets/app.strings.json b/assets/lang/en.json similarity index 88% rename from assets/app.strings.json rename to assets/lang/en.json index 6dd8110..b6be149 100644 --- a/assets/app.strings.json +++ b/assets/lang/en.json @@ -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": { diff --git a/assets/app.palette.json b/assets/palette.json similarity index 100% rename from assets/app.palette.json rename to assets/palette.json diff --git a/assets/textures/font.json b/assets/textures/font.json new file mode 100644 index 0000000..dea029f --- /dev/null +++ b/assets/textures/font.json @@ -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 }, + "�": { "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 } + } +} diff --git a/assets/textures/font.metrics.json b/assets/textures/font.metrics.json deleted file mode 100644 index 603cb37..0000000 --- a/assets/textures/font.metrics.json +++ /dev/null @@ -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 } - } -} diff --git a/assets/textures/font.png b/assets/textures/font.png index 9379489..7356146 100644 Binary files a/assets/textures/font.png and b/assets/textures/font.png differ diff --git a/resources.json b/resources.json index 8766c3f..44c48db 100644 --- a/resources.json +++ b/resources.json @@ -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", diff --git a/resourcestiny.json b/resourcestiny.json index 70555ba..b173760 100644 --- a/resourcestiny.json +++ b/resourcestiny.json @@ -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", diff --git a/schema/metrics.json b/schema/metrics.json index 48117f4..7468916 100644 --- a/schema/metrics.json +++ b/schema/metrics.json @@ -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, diff --git a/schema/resources.json b/schema/resources.json index f6fd7bc..670f371 100644 --- a/schema/resources.json +++ b/schema/resources.json @@ -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", diff --git a/src/common/defs.hpp b/src/common/defs.hpp index bc5c3fc..35819c2 100644 --- a/src/common/defs.hpp +++ b/src/common/defs.hpp @@ -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" diff --git a/src/common/file/file.cpp b/src/common/file/file.cpp index a095297..512acdd 100644 --- a/src/common/file/file.cpp +++ b/src/common/file/file.cpp @@ -244,20 +244,17 @@ const char *StringTable::get(util::Hash id) const { if (!ptr) return _ERROR_STRING; - auto blob = reinterpret_cast(ptr); - auto table = reinterpret_cast(ptr); + auto blob = as(); + auto table = as(); + 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; } diff --git a/src/common/file/file.hpp b/src/common/file/file.hpp index a027a2e..daa0ccd 100644 --- a/src/common/file/file.hpp +++ b/src/common/file/file.hpp @@ -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: diff --git a/src/common/gpufont.cpp b/src/common/gpufont.cpp index 5ed124e..92a3fa6 100644 --- a/src/common/gpufont.cpp +++ b/src/common/gpufont.cpp @@ -15,40 +15,72 @@ */ #include +#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(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(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(&_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(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(&_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(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; } } diff --git a/src/common/gpufont.hpp b/src/common/gpufont.hpp index b99f401..ac29d3a 100644 --- a/src/common/gpufont.hpp +++ b/src/common/gpufont.hpp @@ -16,34 +16,75 @@ #pragma once +#include #include +#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(); + } + 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 diff --git a/src/common/util/misc.hpp b/src/common/util/misc.hpp index e1b5e80..e4a8ad0 100644 --- a/src/common/util/misc.hpp +++ b/src/common/util/misc.hpp @@ -69,6 +69,7 @@ public: bool enable = disableInterrupts(); //assert(enable); + (void) enable; } inline ~ThreadCriticalSection(void) { enableInterrupts(); diff --git a/src/common/util/string.cpp b/src/common/util/string.cpp index d064817..265fed7 100644 --- a/src/common/util/string.cpp +++ b/src/common/util/string.cpp @@ -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( diff --git a/src/common/util/string.hpp b/src/common/util/string.hpp index 21efa71..6e13706 100644 --- a/src/common/util/string.hpp +++ b/src/common/util/string.hpp @@ -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) { diff --git a/src/common/util/string.s b/src/common/util/string.s new file mode 100644 index 0000000..64a3311 --- /dev/null +++ b/src/common/util/string.s @@ -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 . + +.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 diff --git a/src/main/app/app.cpp b/src/main/app/app.cpp index e86a62f..144fa7a 100644 --- a/src/main/app/app.cpp +++ b/src/main/app/app.cpp @@ -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; diff --git a/src/main/app/modals.cpp b/src/main/app/modals.cpp index c10a45a..88afd9c 100644 --- a/src/main/app/modals.cpp +++ b/src/main/app/modals.cpp @@ -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(); - name[0] = CH_DIR_ICON; - path = entries[index].name; + format = CH_DIR_ICON " %s"; + path = entries[index].name; } else { auto entries = _files.as(); - 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; } diff --git a/src/main/app/tests.cpp b/src/main/app/tests.cpp index ba9e5bc..fe12b7b 100644 --- a/src/main/app/tests.cpp +++ b/src/main/app/tests.cpp @@ -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) { diff --git a/src/main/uibase.cpp b/src/main/uibase.cpp index efa41c6..18d8f06 100644 --- a/src/main/uibase.cpp +++ b/src/main/uibase.cpp @@ -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]); diff --git a/src/main/uicommon.cpp b/src/main/uicommon.cpp index c964c67..4ee6d8d 100644 --- a/src/main/uicommon.cpp +++ b/src/main/uicommon.cpp @@ -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); diff --git a/src/main/uicommon.hpp b/src/main/uicommon.hpp index f1f9d1c..5817ac2 100644 --- a/src/main/uicommon.hpp +++ b/src/main/uicommon.hpp @@ -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 ); } diff --git a/src/main/uimodals.cpp b/src/main/uimodals.cpp index e5cb96e..9ceb774 100644 --- a/src/main/uimodals.cpp +++ b/src/main/uimodals.cpp @@ -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 diff --git a/tools/buildCDImage.py b/tools/buildCDImage.py index b37e699..1404576 100755 --- a/tools/buildCDImage.py +++ b/tools/buildCDImage.py @@ -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( diff --git a/tools/buildResourceArchive.py b/tools/buildResourceArchive.py index 7ecdfe1..c981261 100755 --- a/tools/buildResourceArchive.py +++ b/tools/buildResourceArchive.py @@ -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 diff --git a/tools/common/assets.py b/tools/common/assets.py index ae74c94..fdf1f6e 100644 --- a/tools/common/assets.py +++ b/tools/common/assets.py @@ -14,16 +14,14 @@ # You should have received a copy of the GNU General Public License along with # 573in1. If not, see . -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 diff --git a/tools/common/util.py b/tools/common/util.py index 08d3151..b2ba0c7 100644 --- a/tools/common/util.py +++ b/tools/common/util.py @@ -15,10 +15,13 @@ # 573in1. If not, see . 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): diff --git a/tools/decodeDump.py b/tools/decodeDump.py index 2bb204a..b022355 100755 --- a/tools/decodeDump.py +++ b/tools/decodeDump.py @@ -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"