# Adapted from MacNativeGames games/totk/scripts/make_button_icons.py @ 933b6a0 (MIT, Copyright (c) 2026 # MacNativeGames contributors). """Xbox and keyboard-and-mouse button icons for the game's prompts, drawn on the player's machine for tools/make_romfs_patches.py --prompts. The outlines are our own shapes with letters cut out of them in the game's own text fonts: nothing of the game is in the repository. Every prompt except the button guide in play is text in the button font, scft/External.bfotf of each Font/Font*.Nin_NX_NVN.bfarc.zs: private-use code points from U+E040 (A, B, X, Y, then L, R, ZL, ZR, the d-pad, Plus and Minus, the sticks). A .bfotf is an OpenType (CFF) font after an 8-byte header; from offset 4 on, every byte is XORed with a 4-byte key, which the size in the header's second word gives away (bfotf_decode). redraw_font draws the shoulder buttons, the triggers, Plus, Minus and the d-pad as an Xbox controller names them (LB RB, LT RT, Menu and View, a cross), or every button as the key or mouse button the keyboard presses it with (pc_icons), can swap the glyphs of the face buttons (A with B, X with Y), and has fontTools write the font again. The archive then grows by a kilobyte or two, more than the game's resource size table allows it: grow_restbl raises those entries (without it the game stops at start, MacNativeGames found). The button guide in play (bottom right) draws its buttons, L and ZL among them, from images of the common layout archive instead (timg/__Combined.bntx, Nt_KeyTexL_00 and Nt_KeyTexZL_00): bntx_redraw paints the same icons into them, the size unchanged. The Xbox prompts redraw L and ZL, the keyboard and mouse ones every image (PC_HUD_TEXTURES: A, B, X, Y, L, ZL and the stick's two) with the keys of pc_binds. The font's glyphs use pc_binds(text=True): a text prompt shows Y as T, the button guide the left mouse button. Prompts drawn as fixed images (some tutorial animations) keep the Switch icons. Needs fontTools (tools/requirements-installer.txt, tools/requirements-py314.txt). """ import io import struct import zlib from fontTools.pens.basePen import BasePen from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.reverseContourPen import ReverseContourPen from fontTools.pens.t2CharStringPen import T2CharStringPen from fontTools.pens.transformPen import TransformPen from fontTools.ttLib import TTFont FONT = 'scft/External.bfotf' # The letters on the keys come from the text fonts of the default archive; the regional archives lack them. TEXT_ARCHIVE = 'Font/Font.Nin_NX_NVN.bfarc.zs' TEXT_FONTS = ('scft/ninP_RodinNTLG-B_DH_003_subset.bfotf', 'scft/nintendo_NTLG-DB_DH_002.bfotf') # The button guide's images and the glyph each one shows. HUD_TEXTURES = {'Nt_KeyTexL_00': 0xE044, 'Nt_KeyTexZL_00': 0xE046} def xor(data, key): """data with byte i XORed with key[i % 4].""" pad = (key * (len(data) // 4 + 1))[:len(data)] return (int.from_bytes(data, 'little') ^ int.from_bytes(pad, 'little')).to_bytes(len(data), 'little') def bfotf_decode(blob): """(OpenType font, key) of a .bfotf (the module docstring). The font fills the rest of the file.""" key = xor(blob[4:8], struct.pack('>I', len(blob) - 8)) otf = xor(blob[8:], key) assert otf[:4] == b'OTTO', 'not a CFF OpenType font' return otf, key def bfotf_encode(otf, key, head): """The .bfotf of otf: head (the original's first word) and the size, the rest XORed with key as bfotf_decode reads it. The size field holds the whole font, so its length has to be a multiple of 4 (fontTools pads its tables).""" assert len(otf) % 4 == 0, len(otf) return head[:4] + xor(struct.pack('>I', len(otf)) + otf, key) # ---- drawing, in units of the font's 1000-unit em; the round A/B/X/Y icons fill y 20..585 Y0, Y1 = 20, 585 CY, R = (Y0 + Y1) / 2, (Y1 - Y0) / 2 K = 0.5523 # control point distance of a quarter circle drawn with one cubic curve, per unit of radius class Glyphs: """Outlines of letters from the game's own text fonts, drawn as holes in a shape.""" def __init__(self, fonts): self.fonts = fonts def outline(self, ch): for font in self.fonts: cmap = font.getBestCmap() if ord(ch) in cmap: glyphs = font.getGlyphSet(); g = glyphs[cmap[ord(ch)]] rec = RecordingPen(); g.draw(rec); box = BoundsPen(glyphs); g.draw(box) return rec, box.bounds, font['head'].unitsPerEm, g.width raise KeyError(f'no text font has {ch!r}') def hole(self, pen, text, cx, cy, cap, max_width): """text centred on (cx, cy), at most cap high and max_width wide, its contours reversed so they cut holes.""" parts, x, ink = [], 0.0, [1e9, 1e9, -1e9, -1e9] for ch in text: rec, b, upm, advance = self.outline(ch); k = 1000 / upm parts.append((rec, k, x)) ink = [min(ink[0], x + b[0] * k), min(ink[1], b[1] * k), max(ink[2], x + b[2] * k), max(ink[3], b[3] * k)] x += advance * k * 0.96 width, height = ink[2] - ink[0], ink[3] - ink[1] scale = min(cap / height, max_width / width) ox, oy = cx - (ink[0] + width / 2) * scale, cy - (ink[1] + height / 2) * scale reverse = ReverseContourPen(pen) for rec, k, x in parts: rec.replay(TransformPen(reverse, (scale * k, 0, 0, scale * k, ox + x * scale, oy))) def text_glyphs(blobs): """Glyphs over the text fonts in blobs (.bfotf files), the first that has a letter giving it.""" return Glyphs([TTFont(io.BytesIO(bfotf_decode(blob)[0])) for blob in blobs]) def circle(pen, cx, cy, r): k = K * r pen.moveTo((cx + r, cy)) pen.curveTo((cx + r, cy + k), (cx + k, cy + r), (cx, cy + r)) pen.curveTo((cx - k, cy + r), (cx - r, cy + k), (cx - r, cy)) pen.curveTo((cx - r, cy - k), (cx - k, cy - r), (cx, cy - r)) pen.curveTo((cx + k, cy - r), (cx + r, cy - k), (cx + r, cy)) pen.closePath() def rounded(pen, x0, y0, x1, y1, bottom, top=None): """Counter-clockwise rounded rectangle (bottom and top corner radii).""" top = bottom if top is None else top kb, kt = K * bottom, K * top pen.moveTo((x0 + bottom, y0)); pen.lineTo((x1 - bottom, y0)) pen.curveTo((x1 - bottom + kb, y0), (x1, y0 + bottom - kb), (x1, y0 + bottom)); pen.lineTo((x1, y1 - top)) pen.curveTo((x1, y1 - top + kt), (x1 - top + kt, y1), (x1 - top, y1)); pen.lineTo((x0 + top, y1)) pen.curveTo((x0 + top - kt, y1), (x0, y1 - top + kt), (x0, y1 - top)); pen.lineTo((x0, y0 + bottom)) pen.curveTo((x0, y0 + bottom - kb), (x0 + bottom - kb, y0), (x0 + bottom, y0)); pen.closePath() def polygon(pen, points): pen.moveTo(points[0]) for p in points[1:]: pen.lineTo(p) pen.closePath() def xbox_icons(g): """Code point -> (advance, draw(pen)) for the buttons an Xbox controller names differently from a Switch: L R = LB RB, ZL ZR = LT RT, Plus and Minus = Menu and View, the d-pad as a cross. g: Glyphs for the letters. The face buttons keep the game's own glyphs (redraw_font swaps them for the Nintendo layout).""" def bumper(label, advance=820): def draw(pen): rounded(pen, 40, Y0 + 70, advance - 40, Y1 - 70, 70, 150); g.hole(pen, label, advance / 2, CY, 230, advance - 200) return advance, draw def trigger(label, advance=760): def draw(pen): rounded(pen, 60, Y0, advance - 60, Y1, 60, 230); g.hole(pen, label, advance / 2, CY - 20, 230, advance - 220) return advance, draw def menu(advance=640): # three bars, as on the Menu button def draw(pen): circle(pen, advance / 2, CY, R); reverse = ReverseContourPen(pen) for dy in (-115, 0, 115): rounded(reverse, advance / 2 - 150, CY + dy - 38, advance / 2 + 150, CY + dy + 38, 38) return advance, draw def view(advance=640): # two overlapping windows, as on the View button def draw(pen): circle(pen, advance / 2, CY, R); reverse = ReverseContourPen(pen); s = 150 rounded(reverse, advance / 2 - s - 15, CY + 15, advance / 2 - 15, CY + s + 15, 16) rounded(reverse, advance / 2 + 15, CY - s - 15, advance / 2 + s + 15, CY - 15, 16) return advance, draw def dpad(*lit, advance=640): # a cross with an arrow cut into each direction the prompt means def draw(pen): cx, length, arm = advance / 2, R, R * 0.40 polygon(pen, [(cx + length, CY - arm), (cx + length, CY + arm), (cx + arm, CY + arm), (cx + arm, CY + length), (cx - arm, CY + length), (cx - arm, CY + arm), (cx - length, CY + arm), (cx - length, CY - arm), (cx - arm, CY - arm), (cx - arm, CY - length), (cx + arm, CY - length), (cx + arm, CY - arm)]) m, t, h = (length + arm) / 2, arm * 0.72, arm * 0.80 # arm centre, half base and height of the arrow heads = {'up': [(cx - t, CY + m - h / 2), (cx, CY + m + h / 2), (cx + t, CY + m - h / 2)], 'down': [(cx + t, CY - m + h / 2), (cx, CY - m - h / 2), (cx - t, CY - m + h / 2)], 'left': [(cx - m + h / 2, CY - t), (cx - m - h / 2, CY), (cx - m + h / 2, CY + t)], 'right': [(cx + m - h / 2, CY + t), (cx + m + h / 2, CY), (cx + m - h / 2, CY - t)]} for d in lit: polygon(pen, heads[d]) # already clockwise: a hole return advance, draw return { 0xE044: bumper('LB'), 0xE045: bumper('RB'), 0xE046: trigger('LT'), 0xE047: trigger('RT'), 0xE04B: menu(), 0xE04C: view(), 0xE049: dpad('up', 'down', 'left', 'right'), 0xE060: dpad('up'), 0xE061: dpad('down'), 0xE062: dpad('left'), 0xE063: dpad('right'), 0xE064: dpad('up', 'down'), 0xE065: dpad('left', 'right'), } # ---- keyboard and mouse prompts (--prompts pc): every button the keyboard drives, drawn as the key or mouse button # bound to it, in the same filled-shape style. Each glyph keeps the advance of the game's own (a wider glyph runs into # the text of a right-aligned guide). # The keyboard defaults of runtime/input_map.c (input_map_reset; the totk.ini bind_ lines apply on top, pc_binds). The # right stick is the camera, which the mouse drives; SHAKE is the key that shakes it (the mouse never does). PC_BINDS = { 'A': 'e', 'B': 'lshift', 'X': 'space', 'Y': 'mouse_l', 'L': 'q', 'R': 'r', 'ZL': 'mouse_r', 'ZR': 'f', 'PLUS': 'tab', 'MINUS': 'm', 'LEFT': '1', 'UP': '2', 'RIGHT': '3', 'DOWN': '4', 'LCLICK': 'lctrl', 'RCLICK': 'c', 'LSTICK_UP': 'w', 'LSTICK_LEFT': 'a', 'LSTICK_DOWN': 's', 'LSTICK_RIGHT': 'd', 'SHAKE': 'g', } # What a key shows: its letter, a symbol or a short name (key names as in totk.ini; the rest show their name). PC_LABELS = { 'lshift': 'SHIFT', 'rshift': 'SHIFT', 'lctrl': 'CTRL', 'rctrl': 'CTRL', 'lalt': 'ALT', 'tab': 'TAB', 'enter': 'ENT', 'backspace': 'BKSP', 'capslock': 'CAPS', 'insert': 'INS', 'delete': 'DEL', 'home': 'HOME', 'end': 'END', 'pageup': 'PGUP', 'pagedown': 'PGDN', 'up': '↑', 'down': '↓', 'left': '←', 'right': '→', 'num_add': '+', 'num_sub': '-', 'num_mul': '*', 'num_div': '/', 'num_decimal': '.', 'minus': '-', 'equals': '=', 'lbracket': '[', 'rbracket': ']', 'semicolon': ';', 'apostrophe': "'", 'comma': ',', 'period': '.', 'slash': '/', 'backslash': '\\', 'grave': '`', 'mouse_x1': 'M4', 'mouse_x2': 'M5', } # The one-letter panes around the action prompt (T_A_00 ...) are in the Normal font (Rodin B and Symbol): one character # of it per key. PC_LETTER_LABELS = {'lshift': '↑', 'rshift': '↑', 'space': '▬', 'mouse_l': 'L', 'mouse_r': 'R', 'mouse_m': 'M', 'lctrl': 'C', 'rctrl': 'C', 'tab': 'T'} # The button guide's images in the pc style: every symbol mask of the action prompt. In the 1.4.3 Common archive # LSt_00 shows the left stick tilted up (as U+E052) and LSt_01 the left stick (as U+E05E). PC_HUD_TEXTURES = {'Nt_KeyTexA_00': 0xE040, 'Nt_KeyTexB_00': 0xE041, 'Nt_KeyTexX_00': 0xE042, 'Nt_KeyTexY_00': 0xE043, 'Nt_KeyTexL_00': 0xE044, 'Nt_KeyTexZL_00': 0xE046, 'Nt_KeyTexLSt_00': 0xE052, 'Nt_KeyTexLSt_01': 0xE05E} PC_MOUSE = ('mouse_l', 'mouse_r', 'mouse_m') # The text prompts (the button font: the menus' guides, the dialogue log's) show Y as T, the key runtime/input_map.c # binds next to the left mouse button: while a game menu has the pointer the left button is that menu's A # (runtime/menu_cursor.c), and T presses Y everywhere. The button guide in play and the action prompt keep the left # button (PC_BINDS), which attacks there. PC_TEXT_KEYS = {'Y': 't'} def pc_binds(ini_text=None, text=False): """PC_BINDS with the first key of each bind_ line of a totk.ini text over it (a line without a key entry, or with key:default, keeps the default). text: the binds the button font is drawn with, where a button of PC_TEXT_KEYS shows its first key that is not a mouse button (PC_TEXT_KEYS's without a line).""" binds = dict(PC_BINDS, **(PC_TEXT_KEYS if text else {})) for line in (ini_text or '').splitlines(): line = line.split('#')[0].split(';')[0].strip() if not line.lower().startswith('bind_') or '=' not in line: continue action, value = (s.strip() for s in line.split('=', 1)) keys = [e.strip()[4:].strip().lower() for e in value.split(',') if e.strip().lower().startswith('key:')] keys = [k for k in keys if k != 'default'] button = action[5:].upper() if text and button in PC_TEXT_KEYS: keys = [k for k in keys if not k.startswith('mouse_')] or keys if keys and button in binds: binds[button] = keys[0] return binds def font_advances(blob): """Code point -> advance of every glyph of the button font (a .bfotf).""" font = TTFont(io.BytesIO(bfotf_decode(blob)[0])) return {cp: font['hmtx'][name][0] for cp, name in font.getBestCmap().items()} def pc_letter(key): """The one character the panes around the action prompt show for key.""" if key in PC_LETTER_LABELS: return PC_LETTER_LABELS[key] return PC_LABELS.get(key, key.upper())[0] def pc_icons(g, adv, binds=PC_BINDS): """Code point -> (advance, draw(pen)) for every button glyph but Home and Capture, drawn as the key or mouse button binds puts on it. g: Glyphs for the key labels; adv: the game's advance of each glyph (font_advances), which each drawing keeps.""" def mouse_body(pen, cx, y0, y1, lit=None): """A mouse seen from above; lit ('l', 'r', 'm' or None) its pressed button, filled, the others hollow.""" h = y1 - y0; r = h * 0.33; t = h * 0.07; gap = h * 0.035 x0, x1, mid = cx - r, cx + r, y0 + h * 0.52 rounded(pen, x0, y0, x1, y1, r * 0.9, r) reverse = ReverseContourPen(pen) # holes; they never overlap each other or leave the body ri = r - t if lit in ('l', 'r'): # the seams around the pressed button sx0, sx1 = (x0 + t, cx - gap) if lit == 'l' else (cx + gap, x1 - t) polygon(reverse, [(sx0, mid - gap), (sx1, mid - gap), (sx1, mid + gap), (sx0, mid + gap)]) polygon(reverse, [(cx - gap, mid + gap), (cx + gap, mid + gap), (cx + gap, y1 - t), (cx - gap, y1 - t)]) if lit != 'l': # hollow left button, its outer top corner following the body ix0, ix1 = x0 + t, cx - gap reverse.moveTo((ix1, mid + gap)); reverse.lineTo((ix1, y1 - t)); reverse.lineTo((ix0 + ri, y1 - t)) reverse.curveTo((ix0 + ri - K * ri, y1 - t), (ix0, y1 - t - ri + K * ri), (ix0, y1 - t - ri)) reverse.lineTo((ix0, mid + gap)); reverse.closePath() if lit != 'r': ix0, ix1 = cx + gap, x1 - t reverse.moveTo((ix0, mid + gap)); reverse.lineTo((ix1, mid + gap)); reverse.lineTo((ix1, y1 - t - ri)) reverse.curveTo((ix1, y1 - t - ri + K * ri), (ix1 - ri + K * ri, y1 - t), (ix1 - ri, y1 - t)) reverse.lineTo((ix0, y1 - t)); reverse.closePath() if lit == 'm': # the wheel, filled over the gap between the hollow buttons rounded(pen, cx - h * 0.06, mid + h * 0.12, cx + h * 0.06, mid + h * 0.36, h * 0.06) def key_cap(pen, x0, y0, x1, y1, key): """A key: a rounded square with its label cut out; a mouse button draws the mouse.""" if key in PC_MOUSE: mouse_body(pen, (x0 + x1) / 2, y0, y1, key[6:]) return w, h = x1 - x0, y1 - y0 rounded(pen, x0, y0, x1, y1, min(w, h) * 0.18) if key == 'space': # an open bracket lying on its back t = h * 0.09 bx0, bx1, by0, by1 = x0 + w * 0.22, x1 - w * 0.22, y0 + h * 0.36, y0 + h * 0.60 polygon(ReverseContourPen(pen), [(bx0, by0), (bx1, by0), (bx1, by1), (bx1 - t, by1), (bx1 - t, by0 + t), (bx0 + t, by0 + t), (bx0 + t, by1), (bx0, by1)]) return if key in ('lshift', 'rshift'): # a solid arrow and the name under it: the arrow alone read as a direction cx, ab, at, hw, sw = (x0 + x1) / 2, y0 + h * 0.46, y0 + h * 0.86, w * 0.20, w * 0.09 neck = ab + h * 0.18 polygon(ReverseContourPen(pen), [(cx - sw, ab), (cx + sw, ab), (cx + sw, neck), (cx + hw, neck), (cx, at), (cx - hw, neck), (cx - sw, neck)]) g.hole(pen, 'SHIFT', cx, y0 + h * 0.25, h * 0.21, w * 0.84) return label = PC_LABELS.get(key, key.upper()) cap = h * (0.52 if len(label) == 1 else 0.36 if len(label) == 2 else 0.26) g.hole(pen, label, (x0 + x1) / 2, (y0 + y1) / 2, cap, w * 0.74) def arrow(pen, cx, cy, d, s): """A filled arrowhead pointing d, s its half size.""" polygon(pen, {'up': [(cx - s, cy - s * 0.6), (cx + s, cy - s * 0.6), (cx, cy + s * 0.8)], 'down': [(cx + s, cy + s * 0.6), (cx - s, cy + s * 0.6), (cx, cy - s * 0.8)], 'left': [(cx + s * 0.6, cy - s), (cx + s * 0.6, cy + s), (cx - s * 0.8, cy)], 'right': [(cx - s * 0.6, cy + s), (cx - s * 0.6, cy - s), (cx + s * 0.8, cy)]}[d]) def one(key, advance): def draw(pen): side = min(advance - 80, Y1 - Y0) key_cap(pen, (advance - side) / 2, Y0, (advance + side) / 2, Y1, key) return advance, draw def grid(cells, advance): """Small keys in a grid: cells = [(key, column, row)], row 0 on top.""" def draw(pen): cols, rows = max(c for _, c, _ in cells) + 1, max(r for _, _, r in cells) + 1 gap = 24; size = min((advance - 60 - gap * (cols - 1)) / cols, (Y1 - Y0 - gap * (rows - 1)) / rows) left, top = (advance - cols * size - gap * (cols - 1)) / 2, CY + (rows * size + gap * (rows - 1)) / 2 for key, c, r in cells: x, y = left + c * (size + gap), top - (r + 1) * size - r * gap key_cap(pen, x, y, x + size, y + size, key) return advance, draw def mouse_move(advance, *dirs): """The mouse, no button pressed, with an arrow for each direction the prompt means.""" def draw(pen): mouse_body(pen, advance / 2, Y0 + 30, Y1 - 30) off = (Y1 - Y0 - 60) * 0.33 + 70 spots = {'up': (advance / 2 + off, CY + 110), 'down': (advance / 2 + off, CY - 110), 'left': (advance / 2 - off, CY), 'right': (advance / 2 + off, CY)} for d in dirs: arrow(pen, *spots[d], d, 52) return advance, draw b = binds up, left, down, right = b['LSTICK_UP'], b['LSTICK_LEFT'], b['LSTICK_DOWN'], b['LSTICK_RIGHT'] du, dl, dd, dr = b['UP'], b['LEFT'], b['DOWN'], b['RIGHT'] return { **{cp: one(b[name], adv[cp]) for cp, name in ((0xE040, 'A'), (0xE041, 'B'), (0xE042, 'X'), (0xE043, 'Y'), (0xE044, 'L'), (0xE045, 'R'), (0xE046, 'ZL'), (0xE047, 'ZR'), (0xE04B, 'PLUS'), (0xE04C, 'MINUS'), (0xE056, 'LCLICK'), (0xE057, 'RCLICK'))}, # the d-pad: its four keys as a cross (up on top), or the keys of the directions the prompt means 0xE049: grid([(du, 1, 0), (dl, 0, 1), (dd, 1, 1), (dr, 2, 1)], adv[0xE049]), 0xE060: one(du, adv[0xE060]), 0xE061: one(dd, adv[0xE061]), 0xE062: one(dl, adv[0xE062]), 0xE063: one(dr, adv[0xE063]), 0xE064: grid([(du, 0, 0), (dd, 0, 1)], adv[0xE064]), 0xE065: grid([(dl, 0, 0), (dr, 1, 0)], adv[0xE065]), # the left stick: its keys (W A S D) 0xE05E: grid([(up, 1, 0), (left, 0, 1), (down, 1, 1), (right, 2, 1)], adv[0xE05E]), 0xE052: one(up, adv[0xE052]), 0xE050: one(down, adv[0xE050]), 0xE05A: one(left, adv[0xE05A]), 0xE058: one(right, adv[0xE058]), 0xE05C: grid([(up, 0, 0), (down, 0, 1)], adv[0xE05C]), 0xE054: grid([(left, 0, 0), (right, 1, 0)], adv[0xE054]), # the right stick: the mouse 0xE05F: mouse_move(adv[0xE05F]), 0xE053: mouse_move(adv[0xE053], 'up'), 0xE051: mouse_move(adv[0xE051], 'down'), 0xE05B: mouse_move(adv[0xE05B], 'left'), 0xE059: mouse_move(adv[0xE059], 'right'), 0xE05D: mouse_move(adv[0xE05D], 'up', 'down'), 0xE055: mouse_move(adv[0xE055], 'left', 'right'), # the right stick shaken: the shake key 0xE068: one(b['SHAKE'], adv[0xE068]), } def redraw_font(otf, icons, swaps=()): """The OpenType font otf with the glyphs of each (code point, code point) pair of swaps trading places (outlines and horizontal metrics), then every code point of icons (code point -> (advance, draw)) drawn anew, written again by fontTools. The timestamps stay, so the same input gives the same bytes.""" font = TTFont(io.BytesIO(otf), recalcTimestamp=False) assert font['head'].unitsPerEm == 1000, f'em of {font["head"].unitsPerEm} units, the drawings assume 1000' cff = font['CFF '].cff top = cff.topDictIndex[0] assert not hasattr(top, 'ROS'), 'CID-keyed CFF' # its glyphs can use different private dictionaries charstrings, private, hmtx, cmap = top.CharStrings, top.Private, font['hmtx'], font.getBestCmap() nominal = getattr(private, 'nominalWidthX', 0) for a, b in swaps: ga, gb = cmap[a], cmap[b] outline_a, outline_b = charstrings[ga], charstrings[gb] charstrings[ga], charstrings[gb] = outline_b, outline_a hmtx[ga], hmtx[gb] = hmtx[gb], hmtx[ga] for cp, (advance, draw) in icons.items(): pen = T2CharStringPen(advance - nominal, None); draw(pen) # the width operand is relative to nominalWidthX charstrings[cmap[cp]] = pen.getCharString(private=private, globalSubrs=cff.GlobalSubrs) glyphs = font.getGlyphSet() for cp, (advance, _) in icons.items(): box = BoundsPen(glyphs); glyphs[cmap[cp]].draw(box) hmtx[cmap[cp]] = (advance, int(round(box.bounds[0]))) out = io.BytesIO(); font.save(out) return out.getvalue() # ---- the button guide's images: square R8_UNORM masks (white key, black letter), one mip, block-linear, 72x72 up to # 1.2.1 and 108x108 in 1.4.3. Each is drawn from the icon of the same Switch button, fitted into the square. R8_UNORM, HUD_MARGIN, SUPERSAMPLE = 0x201, 4, 4 def gob_offset(x, y, width_gobs, block_height): """Byte offset of (x, y) in a Tegra block-linear surface of 1 byte per pixel (GOBs of 64 bytes x 8 rows).""" return ((y // (8 * block_height)) * 512 * block_height * width_gobs + (x // 64) * 512 * block_height + ((y % (8 * block_height)) // 8) * 512 + ((x % 64) // 32) * 256 + ((y % 8) // 2) * 64 + ((x % 32) // 16) * 32 + (y % 2) * 16 + (x % 16)) class PolygonPen(BasePen): """Outlines flattened to closed polygons.""" def __init__(self): super().__init__(None); self.polygons = [] def _moveTo(self, pt): self.polygons.append([pt]) def _lineTo(self, pt): self.polygons[-1].append(pt) def _curveToOne(self, pt1, pt2, pt3): p0 = self.polygons[-1][-1] for i in range(1, 17): t = i / 16; u = 1 - t self.polygons[-1].append(tuple(u ** 3 * p0[k] + 3 * u * u * t * pt1[k] + 3 * u * t * t * pt2[k] + t ** 3 * pt3[k] for k in (0, 1))) def _qCurveToOne(self, pt1, pt2): p0 = self.polygons[-1][-1] for i in range(1, 17): t = i / 16; u = 1 - t self.polygons[-1].append(tuple(u * u * p0[k] + 2 * u * t * pt1[k] + t * t * pt2[k] for k in (0, 1))) def rasterize(draw, size): """A size x size coverage mask (0..255, rows top down) of draw's outline, fitted into the square, nonzero winding.""" rec = RecordingPen(); draw(rec) box = BoundsPen(None); rec.replay(box); x0, y0, x1, y1 = box.bounds inner = size - 2 * HUD_MARGIN; scale = inner / max(x1 - x0, y1 - y0) ox = HUD_MARGIN + (inner - (x1 - x0) * scale) / 2 - x0 * scale oy = HUD_MARGIN + (inner - (y1 - y0) * scale) / 2 - y0 * scale flat = PolygonPen(); rec.replay(TransformPen(flat, (scale, 0, 0, -scale, ox, size - oy))) # font y up -> rows down edges = [(a, b) for poly in flat.polygons for a, b in zip(poly, poly[1:] + poly[:1]) if a[1] != b[1]] n = size * SUPERSAMPLE; cover = [0] * (size * size) for row in range(n): y = (row + 0.5) / SUPERSAMPLE crossings = sorted((a[0] + (y - a[1]) * (b[0] - a[0]) / (b[1] - a[1]), 1 if b[1] > a[1] else -1) for a, b in edges if min(a[1], b[1]) <= y < max(a[1], b[1])) winding, start = 0, None for x, d in crossings: if winding == 0: start = x winding += d if winding == 0: for col in range(max(0, round(start * SUPERSAMPLE)), min(n, round(x * SUPERSAMPLE))): cover[(row // SUPERSAMPLE) * size + col // SUPERSAMPLE] += 1 return bytes(min(255, c * 255 // (SUPERSAMPLE * SUPERSAMPLE)) for c in cover) def bntx_redraw(data, base, textures, icons): """Paint the icon of each texture of textures (name without the ^ suffix -> code point; icons: code point -> (advance, draw)) over its image in the BNTX at data[base:] (a bytearray), in place. Returns the names painted.""" assert data[base:base + 4] == b'BNTX' and data[base + 0x20:base + 0x24] == b'NX ' count, table = struct.unpack_from('.Nin_NX_NVN.rsizetable.zs, a RESTBL whose # entries are keyed by the CRC32 of the RomFS path without .zs. The game sizes the buffer it loads a file into from its # entry, so an archive that grew needs its entry raised by as much. def restbl_offsets(table): """CRC32 of a path -> offset of its size in the RESTBL table.""" assert table[:6] == b'RESTBL', 'not a RESTBL' count = struct.unpack_from(' bytes it grew) raised by that much.""" at, out = restbl_offsets(table), bytearray(table) for path, extra in growth.items(): h = zlib.crc32(path.removesuffix('.zs').encode()) assert h in at, f'{path} has no entry in the resource size table' struct.pack_into('