Linux: skip menu fonts that have no Latin letters #1405

Merged
berelels merged 2 commits from fix/menu-font-latin into main 2026-11-10 04:39:20 +00:00
berelels commented 2026-11-10 00:53:07 +00:00 (Migrated from github.com)

What

On the native Linux build the Insert menu draws its text with stb_truetype from the first font that loads: one beside
the game (fonts/regular.ttf, fonts/bold.ttf), then fontconfig's sans-serif, then a few usual paths
(host/posix/gdi_posix.c). Nothing checked that the font has Latin letters. A font without them draws every letter
as its empty box (.notdef, glyph 0): rows of hollow squares, while the same folder under Wine reads fine (#1279).

  • A font without a glyph for A is now passed over for the next candidate.
  • fontconfig is asked for sans-serif:lang=en (and sans-serif:bold:lang=en): its choice among the fonts that
    have the menu's letters.

I could not tell which font the reporter's system returns; with fonts on my machine, stb_truetype draws exactly that
box for every font without Latin letters.

Tests

  • tests/test_menu_font_latin.py (new, Linux only, skipped on Windows): builds two tiny TrueType fonts, one with
    A and one with only a Hebrew letter. The second goes beside the game, and a stand-in fc-match on PATH answers
    with the first. gdi_posix.c is compiled with a fixture that draws "A" in both weights: both must draw pixels,
    and fc-match must be asked with lang=en. No system font is used. It fails on the current code and with
    either half of the change taken out.
  • Full suite on Linux: the same failures as on main (my environment); nothing new fails.
  • Not tested: the game itself, and the reporter's system.

Refs #1279

AI assistance

Assisted-by: Claude Opus 5.5
Assisted-by: Nemotron 3 Ultra 550B (LLM council)
Assisted-by: gpt-oss-120b (LLM council)
Assisted-by: dots-3-note-preview (LLM council)
Assisted-by: Gemini 3.5 Flash-Lite (LLM council)
Co-Authored-By: Claude Opus 5.5 [email protected]

The LLM council (several models answer the same question independently, then
rank each other's answers anonymously) was used to define the approach and the
solution: which problem to take on, how to fix it and what to avoid. It saw a
summary of the issues, not the code. The change and its test were written with
Claude Code, then reviewed and tested by me.

## What On the native Linux build the Insert menu draws its text with stb_truetype from the first font that loads: one beside the game (`fonts/regular.ttf`, `fonts/bold.ttf`), then fontconfig's `sans-serif`, then a few usual paths (`host/posix/gdi_posix.c`). Nothing checked that the font has Latin letters. A font without them draws every letter as its empty box (.notdef, glyph 0): rows of hollow squares, while the same folder under Wine reads fine (#1279). - A font without a glyph for `A` is now passed over for the next candidate. - fontconfig is asked for `sans-serif:lang=en` (and `sans-serif:bold:lang=en`): its choice among the fonts that have the menu's letters. I could not tell which font the reporter's system returns; with fonts on my machine, stb_truetype draws exactly that box for every font without Latin letters. ## Tests - `tests/test_menu_font_latin.py` (new, Linux only, skipped on Windows): builds two tiny TrueType fonts, one with `A` and one with only a Hebrew letter. The second goes beside the game, and a stand-in `fc-match` on PATH answers with the first. `gdi_posix.c` is compiled with a fixture that draws "A" in both weights: both must draw pixels, and `fc-match` must be asked with `lang=en`. No system font is used. It fails on the current code and with either half of the change taken out. - Full suite on Linux: the same failures as on `main` (my environment); nothing new fails. - Not tested: the game itself, and the reporter's system. Refs #1279 ## AI assistance Assisted-by: Claude Opus 5.5 Assisted-by: Nemotron 3 Ultra 550B (LLM council) Assisted-by: gpt-oss-120b (LLM council) Assisted-by: dots-3-note-preview (LLM council) Assisted-by: Gemini 3.5 Flash-Lite (LLM council) Co-Authored-By: Claude Opus 5.5 <[email protected]> The LLM council (several models answer the same question independently, then rank each other's answers anonymously) was used to define the approach and the solution: which problem to take on, how to fix it and what to avoid. It saw a summary of the issues, not the code. The change and its test were written with Claude Code, then reviewed and tested by me.
hckrman101 (Migrated from github.com) reviewed 2026-11-10 00:53:07 +00:00
NotRexed commented 2026-11-10 03:49:37 +00:00 (Migrated from github.com)

This landed in the v2.0.12 branch (merged there for testing; it reaches main with that release), so closing. Thanks!

This landed in the v2.0.12 branch (merged there for testing; it reaches `main` with that release), so closing. Thanks!
NotRexed commented 2026-11-10 03:55:06 +00:00 (Migrated from github.com)

Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise.

Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise.
hckrman101 (Migrated from github.com) approved these changes 2026-11-10 04:17:30 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks good. The stbtt_FindGlyphIndex(..., 'A') check in face_load is cheap and does what the description says. On failure the buffer is freed and ok stays 0, so the next candidate (fontconfig, then the fixed paths) is tried cleanly. The test is self-contained: it builds two tiny fonts, and I checked that host/posix/windows.h declares GetModuleFileNameA and that third_party/stb is on the include path. It is registered in run_all.py and skipped on Windows.

Overlap with the sibling PRs (no semantic conflict, but expect textual ones):

  • #1371 (Fontconfig face) rewrites the same code: face_load gets an index parameter and fc_match becomes face_match(face, pattern). The face_load and faces_load hunks here will conflict. After rebasing, the 'A' check goes after stbtt_InitFont with the indexed offset, and the patterns become face_match(&faces[0], "sans-serif:lang=en") and "sans-serif:bold:lang=en". They combine fine: that PR picks the face inside a collection, this one rejects a face with no Latin. Whichever lands second has to rework its hunks, and the run_all.py hunks are adjacent in both.
  • #1389 (UTF-8 names) only touches the text-drawing half of gdi_posix.c (and vk_menu.cpp), so no conflict in code. Only run_all.py is adjacent. They are independent in behaviour. Once #1389 draws non-Latin names, a Latin-only font still shows boxes for those names. That is outside this PR.

Nits:

  • A fonts/regular.ttf beside the game that has no 'A' is now skipped silently. A one-line stderr note would help someone who drops in a CJK-only font and wonders why it is ignored.
  • The lang=en change relies on fc-match preferring fonts that cover English. That is its documented behaviour, but the glyph check is what really protects you. The test covers both halves, which is nice.

CI: the Linux-relevant jobs I could see (appimage, Windows Vulkan build, actionlint) pass. Python source checks and a few others were still pending when I looked. I did not build or run the PR.

Looks good. The `stbtt_FindGlyphIndex(..., 'A')` check in `face_load` is cheap and does what the description says. On failure the buffer is freed and `ok` stays 0, so the next candidate (fontconfig, then the fixed paths) is tried cleanly. The test is self-contained: it builds two tiny fonts, and I checked that `host/posix/windows.h` declares `GetModuleFileNameA` and that `third_party/stb` is on the include path. It is registered in `run_all.py` and skipped on Windows. Overlap with the sibling PRs (no semantic conflict, but expect textual ones): - #1371 (Fontconfig face) rewrites the same code: `face_load` gets an `index` parameter and `fc_match` becomes `face_match(face, pattern)`. The `face_load` and `faces_load` hunks here will conflict. After rebasing, the `'A'` check goes after `stbtt_InitFont` with the indexed offset, and the patterns become `face_match(&faces[0], "sans-serif:lang=en")` and `"sans-serif:bold:lang=en"`. They combine fine: that PR picks the face inside a collection, this one rejects a face with no Latin. Whichever lands second has to rework its hunks, and the `run_all.py` hunks are adjacent in both. - #1389 (UTF-8 names) only touches the text-drawing half of `gdi_posix.c` (and `vk_menu.cpp`), so no conflict in code. Only `run_all.py` is adjacent. They are independent in behaviour. Once #1389 draws non-Latin names, a Latin-only font still shows boxes for those names. That is outside this PR. Nits: - A `fonts/regular.ttf` beside the game that has no `'A'` is now skipped silently. A one-line stderr note would help someone who drops in a CJK-only font and wonders why it is ignored. - The `lang=en` change relies on fc-match preferring fonts that cover English. That is its documented behaviour, but the glyph check is what really protects you. The test covers both halves, which is nice. CI: the Linux-relevant jobs I could see (appimage, Windows Vulkan build, actionlint) pass. Python source checks and a few others were still pending when I looked. I did not build or run the PR.
hckrman101 commented 2026-11-10 04:37:01 +00:00 (Migrated from github.com)

I think #1459 completely replaces this btw

I think https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/1459 completely replaces this btw
Commenting is not possible because the repository is archived.
No description provided.