Merge #900: lay the action prompt's diamond out as an Xbox pad with the xbox-label prompts

This commit is contained in:
Valiantsin Dzerakh committed 2026-11-07 12:38:12 +03:00
commit 71d8b0cc61
2 files changed
+94 -4

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
| Plus, Minus | Menu, View | Menu, View |
| D-pad | cross | cross |
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters stay and the diamond is laid out as an Xbox pad: A at the bottom, B on the right, X on the left and Y at the top, the button the prompt names lit in its place, its bar reaching left from X and right from the others (`tools/make_romfs_patches.py`, `bflyt_xbox_diamond`: the panes of `blyt/DoCommand_00.bflyt` take their partner's place, A's of B and X's of Y). The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
+93 -3
View File
@@ -35,8 +35,9 @@ from the game's own files on the player's machine, nothing added (tools/button_i
stays right.
- every other prompt: the glyphs U+E040 A, U+E041 B, U+E042 X, U+E043 Y of the button font trade places.
- xbox-label, for pad_layout = xbox and auto. Those layouts put Switch A on the button labelled A (auto picks xbox for
every pad not labelled the Nintendo way), so the face buttons keep their letters; the action prompt's diamond keeps
the Switch positions (A on the right).
every pad not labelled the Nintendo way), so the face buttons keep their letters. The action prompt's diamond is
laid out as an Xbox pad's (bflyt_xbox_diamond): A at the bottom, B on the right, X on the left, Y at the top, the
button the prompt names lit in its place with its bar.
--xbox-prompts is the older name of --prompts xbox.
--prompts pc: keyboard and mouse prompts, for prompts = pc (romfs-patches-pc, or romfs-patches-pc-<w>x<h> with
--aspect): every button of the button font is drawn as the key or mouse button the keyboard defaults put on it
@@ -170,6 +171,92 @@ def bflyt_letters(data, base, end, swap):
return changed
def bflyt_panes(data, base, end):
"""name -> absolute offset of each pane section (pan1, pic1, txt1, wnd1, ...) of the BFLYT at data[base:end], and
name -> absolute offset of each material of its mat1, and its texture names (txl1)."""
assert data[base:base + 4] == b'FLYT'
pos, panes, mats, textures = base + struct.unpack_from('<H', data, base + 6)[0], {}, {}, []
while pos + 8 <= end:
magic, size = data[pos:pos + 4], struct.unpack_from('<I', data, pos + 4)[0]
if magic in (b'pan1', b'pic1', b'txt1', b'wnd1', b'bnd1', b'prt1', b'ali1', b'scr1'):
panes[data[pos + 12:pos + 36].split(b'\0')[0].decode()] = pos
elif magic == b'mat1':
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
at = pos + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
mats[data[at:at + 28].split(b'\0')[0].decode()] = at
elif magic == b'txl1':
for i in range(struct.unpack_from('<H', data, pos + 8)[0]):
at = pos + 12 + struct.unpack_from('<I', data, pos + 12 + 4 * i)[0]
textures.append(data[at:data.index(0, at)].decode())
if not size:
break
pos += size
return panes, mats, textures
# The action prompt's diamond (blyt/DoCommand_00.bflyt) for pad_layout = xbox and auto, where Switch A, B, X and Y are
# the buttons labelled so on an Xbox pad: A at the bottom, B on the right, X on the left, Y at the top. The layout has
# one diamond per button the prompt can name (N_A_00, N_B_00, N_X_00, N_Y_00; N_StopA_00 is A's while held), each
# with its button and bar (N_Btn<K>_00), the three other letters (T_<letter>_<n>) and a base image (P_Base<K>_00)
# that lights the button's side. A Switch button's place on an Xbox pad is that of its partner on a Switch (A of B,
# X of Y), so each piece takes the place and look its partner's piece has: the bar of a button on the left reaches
# left (Y's on a Switch), the others reach right.
DIAMOND_SLOTS = {'A': (39.0, 1.0), 'B': (2.0, -39.0), 'X': (1.0, 39.0), 'Y': (-38.0, 0.0)} # the letters' places
DIAMOND_PARTNER = {'A': 'B', 'B': 'A', 'X': 'Y', 'Y': 'X'}
# (pane, the pane whose place it takes): the buttons with their bars, and the flashes of a repeated press (Rush)
DIAMOND_MOVES = [('N_BtnA_00', 'N_BtnB_00'), ('N_BtnA_01', 'N_BtnB_00'), ('N_CompanionBtnA_00', 'N_BtnB_00'),
('N_BtnB_00', 'N_BtnA_00'), ('N_BtnX_00', 'N_BtnY_00'), ('N_BtnY_00', 'N_BtnX_00')]
DIAMOND_MOVES += [(f'P_Capture{k}_{n:02}', f'P_Capture{DIAMOND_PARTNER[k]}_{n:02}') for k in 'ABXY' for n in (1, 2, 3)]
# X and Y trade the side their bar reaches to: the flash, the bar (Y's mirrored) and the text (Y's right-aligned)
DIAMOND_MIRROR = [('P_CaptureX_00', 'P_CaptureY_00'), ('W_BaseX_00', 'W_BaseY_00'), ('T_GuideX_00', 'T_GuideY_00'),
('T_GuideX_TW', 'T_GuideY_TW')]
# the base image: its texture (by its material) and its rotation and scale
DIAMOND_BASES = [('P_BaseA_00', 'P_BaseB_00'), ('P_BaseA_01', 'P_BaseB_00'), ('P_BaseB_00', 'P_BaseA_00'),
('P_BaseX_00', 'P_BaseY_00'), ('P_BaseY_00', 'P_BaseX_00')]
def bflyt_xbox_diamond(data, base, end):
"""Lay the action prompt's diamond in the BFLYT at data[base:end] (blyt/DoCommand_00.bflyt) out as an Xbox pad's
face buttons (DIAMOND_* above); returns the panes changed. A pane section holds its origin at +0x09, translation
(3 floats) at +0x2C, rotation (3) at +0x38 and scale (2) at +0x44; a text pane its text position at +0x5C; a
picture pane its material index at +0x64, and a material its first texture's index at +0x2C."""
panes, mats, textures = bflyt_panes(data, base, end)
old = bytes(data) # every piece takes its partner's place as the game has it
changed = []
for name, at in panes.items():
m = re.fullmatch(r'T_([ABXY])_\d+', name)
if m: # a letter around a diamond: its partner's place
struct.pack_into('<2f', data, at + 0x2C, *DIAMOND_SLOTS[DIAMOND_PARTNER[m[1]]])
changed.append(name)
for name, like in DIAMOND_MOVES:
data[panes[name] + 0x2C:panes[name] + 0x34] = old[panes[like] + 0x2C:panes[like] + 0x34]
changed.append(name)
# A's hold flashes (N_LoopA_03) sit 129 right of the diamond, as A's repeated-press ones: the same move as those
dx, dy = (a - b for a, b in zip(struct.unpack_from('<2f', old, panes['P_CaptureB_01'] + 0x2C),
struct.unpack_from('<2f', old, panes['P_CaptureA_01'] + 0x2C)))
x, y = struct.unpack_from('<2f', old, panes['N_LoopA_03'] + 0x2C)
struct.pack_into('<2f', data, panes['N_LoopA_03'] + 0x2C, x + dx, y + dy)
changed.append('N_LoopA_03')
for a, b in DIAMOND_MIRROR:
pa, pb = panes[a], panes[b]
for lo, hi in ((0x09, 0x0A), (0x2C, 0x4C)) + (((0x5C, 0x5D),) if old[pa:pa + 4] == b'txt1' else ()):
data[pa + lo:pa + hi], data[pb + lo:pb + hi] = old[pb + lo:pb + hi], old[pa + lo:pa + hi]
changed += [a, b]
in_order = list(mats.values())
def material(name): # the offset of a picture pane's material
return in_order[struct.unpack_from('<H', old, panes[name] + 0x64)[0]]
for name, like in DIAMOND_BASES:
assert old[panes[name]:panes[name] + 4] == b'pic1', name
data[panes[name] + 0x38:panes[name] + 0x4C] = old[panes[like] + 0x38:panes[like] + 0x4C]
mine, theirs = material(name), material(like)
data[mine + 0x2C:mine + 0x2E] = old[theirs + 0x2C:theirs + 0x2E]
assert textures[struct.unpack_from('<H', data, mine + 0x2C)[0]].startswith('Nt_KeyBtnSubBase'), name
changed.append(name)
return changed
def msbt_letters(data, swap):
"""Swap the one-letter messages labelled T_<letter>_<n> (A, B, X or Y) in the UTF-16 MSBT data (a bytearray) by
swap; the labels changed. The game sets a layout's text panes from the messages of the same name in
@@ -319,6 +406,9 @@ if args.prompts:
letters = {}
if by_position:
bntx_swap(blarc, bntx, KEY_TEXTURES)
diamond = []
if args.prompts == 'xbox-label':
diamond = bflyt_xbox_diamond(blarc, *members['blyt/DoCommand_00.bflyt'])
if letter_panes:
for member, (lo, hi) in members.items():
if member.endswith('.bflyt'):
@@ -350,7 +440,7 @@ if args.prompts:
if grown:
outputs[RESTBL] = icons.grow_restbl(unpack(RESTBL), grown)
prompts = dict(style=args.prompts, textures=KEY_TEXTURES if by_position else [], letters=letters,
messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
diamond=len(diamond), messages=messages, hud=hud, icons=len(drawn), fonts=FONTS, grown=grown)
lines = []
for path, contents in outputs.items():