Skip to content

Commit 98c029c

Browse files
Added font viewer
1 parent 228cb98 commit 98c029c

6 files changed

Lines changed: 471 additions & 12 deletions

File tree

icons/totk-icon-theme.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
},
3434
"_totk_tkvsc": {
3535
"iconPath": "./tkvsc.svg"
36+
},
37+
"_totk_font": {
38+
"iconPath": "./font.svg"
3639
}
3740
},
3841
"folderNames": {
@@ -150,7 +153,9 @@
150153
"bshop.zs": "_totk_aamp",
151154
"txtg": "_totk_texture",
152155
"txtg.zs": "_totk_texture",
153-
"tkvsc": "_totk_tkvsc"
156+
"tkvsc": "_totk_tkvsc",
157+
"bfttf": "_totk_font",
158+
"bfotf": "_totk_font"
154159
},
155160
"fileNames": {
156161
".tkproj": "_totk_tkproj",
@@ -161,6 +166,7 @@
161166
"bgyml": "_totk_bgyml",
162167
"msbt": "_totk_msbt",
163168
"totk-xlnk": "_totk_xlnk",
164-
"totk-tkvsc": "_totk_tkvsc"
169+
"totk-tkvsc": "_totk_tkvsc",
170+
"totk-font": "_totk_font"
165171
}
166172
}

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@
5050
}
5151
],
5252
"priority": "default"
53+
},
54+
{
55+
"viewType": "totk-editor.fontViewer",
56+
"displayName": "TOTK Font Viewer",
57+
"selector": [
58+
{
59+
"filenamePattern": "*.bfttf"
60+
},
61+
{
62+
"filenamePattern": "*.bfotf"
63+
}
64+
],
65+
"priority": "default"
5366
}
5467
],
5568
"iconThemes": [
@@ -172,6 +185,20 @@
172185
"dark": "./icons/xlnk.svg"
173186
}
174187
},
188+
{
189+
"id": "totk-font",
190+
"extensions": [
191+
".bfttf",
192+
".bfotf"
193+
],
194+
"aliases": [
195+
"TOTK Font"
196+
],
197+
"icon": {
198+
"light": "./icons/font.svg",
199+
"dark": "./icons/font.svg"
200+
}
201+
},
175202
{
176203
"id": "pchtxt",
177204
"extensions": [
@@ -228,13 +255,13 @@
228255
},
229256
{
230257
"command": "totk-editor.canonicalSyncOn",
231-
"title": "Enable Canonical Sync",
232-
"icon": "./icons/outline-view-active.svg"
258+
"title": "Canonical Sync: Enabled",
259+
"icon": "./icons/canon_active.svg"
233260
},
234261
{
235262
"command": "totk-editor.canonicalSyncOff",
236-
"title": "Enable Canonical Sync",
237-
"icon": "./icons/outline-view-inactive.svg"
263+
"title": "Canonical Sync: Disabled",
264+
"icon": "./icons/canon_inactive.svg"
238265
},
239266
{
240267
"command": "totk-editor.removeArchiveRoot",
@@ -610,7 +637,12 @@
610637
"properties": {
611638
"totk-editor.logLevel": {
612639
"type": "string",
613-
"enum": ["Debug", "Info", "Warning", "Error"],
640+
"enum": [
641+
"Debug",
642+
"Info",
643+
"Warning",
644+
"Error"
645+
],
614646
"default": "Info",
615647
"markdownDescription": "Determine the verbosity of logs written to the Output Channel."
616648
},
@@ -636,7 +668,10 @@
636668
},
637669
"totk-editor.tagProductFormat": {
638670
"type": "string",
639-
"enum": ["json", "yaml"],
671+
"enum": [
672+
"json",
673+
"yaml"
674+
],
640675
"default": "yaml",
641676
"markdownDescription": "Format to use for displaying Tag.Product.rstbl.byml files (JSON or YAML)."
642677
},
@@ -663,8 +698,7 @@
663698
"items": {
664699
"type": "string"
665700
},
666-
"default": [
667-
],
701+
"default": [],
668702
"markdownDescription": "Canonical path folder prefixes excluded from synchronized save propagation. Matching is case-insensitive and checks full canonical path prefixes. This is NOT the RomFS path!"
669703
},
670704
"totk-editor.canonicalSyncArchiveTypeBlacklist": {
@@ -816,6 +850,7 @@
816850
"devDependencies": {
817851
"@types/mocha": "^10.0.10",
818852
"@types/node": "^25.9.1",
853+
"@types/opentype.js": "^1.3.10",
819854
"@types/vscode": "^1.120.0",
820855
"@vscode/test-cli": "^0.0.12",
821856
"@vscode/test-electron": "^2.5.2",
@@ -827,6 +862,7 @@
827862
"typescript-eslint": "^8.60.0"
828863
},
829864
"dependencies": {
865+
"opentype.js": "^2.0.0",
830866
"sql.js": "^1.14.1"
831867
}
832868
}

python/archive_resolve.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,49 @@
1212
_ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"
1313

1414

15+
def decrypt_bfttf(data: bytes) -> bytes:
16+
if len(data) <= 8:
17+
return data
18+
magic = data[:4]
19+
if magic == b"\xd9\x9b\x87\x1a":
20+
base_key = 2785117442
21+
elif magic == b"\x36\xf8\x1a\x1e":
22+
base_key = 1231165446
23+
elif magic == b"\xf3\x68\xde\xc1":
24+
base_key = 2364726489
25+
else:
26+
return data
27+
28+
first_chunk = int.from_bytes(data[8:12], byteorder="big")
29+
possible_magics = [
30+
0x4F54544F,
31+
0x00010000,
32+
0x774F4646,
33+
0x774F4632,
34+
0x74727565,
35+
0x74746366,
36+
]
37+
38+
file_size_val = len(data) - 8
39+
derived_key = base_key ^ file_size_val
40+
if (first_chunk ^ derived_key) in possible_magics:
41+
key_val = derived_key
42+
elif (first_chunk ^ base_key) in possible_magics:
43+
key_val = base_key
44+
else:
45+
key_val = base_key
46+
for pm in possible_magics:
47+
if (first_chunk ^ pm) ^ base_key < 0x0FFFFFFF:
48+
key_val = first_chunk ^ pm
49+
break
50+
51+
out = bytearray(len(data) - 8)
52+
key_bytes = key_val.to_bytes(4, byteorder="big")
53+
for i in range(8, len(data)):
54+
out[i - 8] = data[i] ^ key_bytes[i % 4]
55+
return bytes(out)
56+
57+
1558
def _is_archive_name(name: str) -> bool:
1659
return bool(_ARCHIVE_SEGMENT.search(name.replace("\\", "/")))
1760

@@ -303,7 +346,10 @@ def read_archive_file_bytes(disk_archive_path: str, file_path: str, romfs_path:
303346
return read_texture_data(bntx_data, remainder)
304347

305348
sarc, lookup, _, _ = resolve_sarc_view(disk_archive_path, file_path, romfs_path)
306-
return _get_file_bytes(sarc, lookup)
349+
file_bytes = _get_file_bytes(sarc, lookup)
350+
if file_path.lower().endswith((".bfotf", ".bfttf")):
351+
file_bytes = decrypt_bfttf(file_bytes)
352+
return file_bytes
307353

308354

309355
def _reject_bntx_mutation(disk_archive_path: str, operation: str, target_path: str = "") -> None:

src/extension.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
import { getCoreExtensions, initCoreFsExtensions } from './coreFsExtensions';
5454
import { TkprojEditorProvider } from './tkprojEditor';
5555
import { TkvscEditorProvider } from './tkvscEditor';
56+
import { FontViewerProvider } from './fontViewer';
5657
import { setExtensionPath } from './romfsIndex';
5758
import {
5859
hasBaseCanonicalPath,
@@ -748,9 +749,37 @@ export async function activate(context: vscode.ExtensionContext) {
748749
registerDocumentLanguageModes(context);
749750
context.subscriptions.push(TkprojEditorProvider.register(context));
750751
context.subscriptions.push(TkvscEditorProvider.register(context));
751-
752+
752753
const bridgePath = path.join(context.extensionPath, 'python', 'totk_bridge.py');
753754
const getPython = () => getCachedPythonExecutable() ?? '';
755+
756+
const getRawFontBytes = async (uri: vscode.Uri): Promise<Uint8Array> => {
757+
if (uri.scheme === 'file') {
758+
return await fs.promises.readFile(uri.fsPath);
759+
}
760+
761+
const fsPath = uri.fsPath;
762+
const diskArchive = getDiskArchivePath(fsPath);
763+
const locator = getLocatorInsideDiskArchive(fsPath, diskArchive);
764+
765+
if (!locator || diskArchive === fsPath) {
766+
return await fs.promises.readFile(fsPath);
767+
}
768+
769+
const result = await runBridgeJsonAsync<{ path: string }>(
770+
getPython(),
771+
bridgePath,
772+
['export-temp', diskArchive, locator],
773+
undefined,
774+
getBridgeEnv()
775+
);
776+
const raw = await fs.promises.readFile(result.path);
777+
try {
778+
fs.unlinkSync(result.path);
779+
} catch {}
780+
return raw;
781+
};
782+
context.subscriptions.push(FontViewerProvider.register(context, getRawFontBytes));
754783
const romfsIndexPath = path.join(context.globalStorageUri.fsPath, 'romfs-index.sqlite');
755784
const canonicalIndexPath = path.join(context.globalStorageUri.fsPath, 'canonical-paths.sqlite');
756785
const projectCanonicalOverlayPath = path.join(

0 commit comments

Comments
 (0)