add ttf tiles option

lets a translation keep chosen characters on the rom tiles instead of the ttf.
needed for japanese: sizing the font for kana makes latin narrower, which
knocks the party menu numbers out of line.
This commit is contained in:
bryanthaboi
2026-08-04 09:11:05 -04:00
parent 6cfc85ca83
commit 33998aab5d
4 changed files with 86 additions and 6 deletions
+11 -2
View File
@@ -386,8 +386,17 @@ Vautour (Burpy Fresh), licensed under CC-BY 4.0 (5x11 base characters,
the registry entry: `file` for a mod-shipped TTF, `size` (the font's
design em; Plain Pixel rasterizes cleanly only at multiples of 15),
`spacing` added to every advance, `yOffset` for vertical alignment
against the 8px cell grid, and `bold`, which double-prints at a 1px
offset for fonts whose strokes read too light.
against the 8px cell grid, `bold`, which double-prints at a 1px
offset for fonts whose strokes read too light, and `tiles`, the
characters that keep their ROM tile instead of coming from the TTF.
`tiles` matters for a CJK translation. Sizing the font so a kana fills
the 8px cell leaves Latin narrower than the tile font it replaces, which
pulls the numeric columns out of line: the party menu's `:L12` stops
sitting over `34/ 34`. Naming `"0123456789/:"` keeps those on the
vanilla tiles, so numbers render exactly as they do in English while
kana still come from the font. It takes a string of characters, or a
list when a multi-character charmap sequence is meant.
See the wiki's Translations guide.
+6 -2
View File
@@ -1079,7 +1079,11 @@ R.font = {
f.rec{ seq = f.str, code = f.int(0) },
f.rec{ file = f.opt(f.path), size = f.opt(f.int(1)),
spacing = f.opt(f.num), yOffset = f.opt(f.num),
bold = f.opt(f.bool) },
bold = f.opt(f.bool),
-- characters that keep their ROM tile instead of coming from the
-- TTF: a string of them, or a list when a multi-character charmap
-- sequence is meant (src/render/Font.lua)
tiles = f.opt(f.union{ f.str, f.list(f.str) }) },
},
extra = function(id, value)
if fontIsCharmap(id) then
@@ -1092,7 +1096,7 @@ R.font = {
elseif fontIsTtf(id) then
-- every field optional: {} is "the bundled font at its native size"
if value.image ~= nil or value.base ~= nil then
return 'the "ttf" entry takes file/size/spacing/yOffset/bold, not a page'
return 'the "ttf" entry takes file/size/spacing/yOffset/bold/tiles, not a page'
end
elseif value.image == nil or value.base == nil then
return "a font page needs an image and a base"
+40 -2
View File
@@ -55,6 +55,34 @@ local function pagesOf(def)
return pages
end
-- ttf.tiles as a lookup keyed by charmap sequence. Accepts a plain string
-- ("0123456789"), which is split into UTF-8 characters, or a list of
-- sequences ({ "0", "1", "<PK>" }) when a multi-character macro is meant.
local function tileSet(spec)
local set = {}
if type(spec) == "table" then
for _, seq in ipairs(spec) do set[tostring(seq)] = true end
elseif type(spec) == "string" then
-- split on UTF-8 lead bytes rather than utf8Decode, which this file
-- declares further down and would be nil here
local i, n = 1, #spec
while i <= n do
local last = i
if spec:byte(i) >= 0xC0 then
local k = i + 1
while k <= n do
local b = spec:byte(k)
if b < 0x80 or b > 0xBF then break end
last, k = k, k + 1
end
end
set[spec:sub(i, last)] = true
i = last + 1
end
end
return set
end
function Font.load(data)
loadedFrom = data
local def = data.font
@@ -128,6 +156,15 @@ function Font.load(data)
-- caps line up and descenders hang below as the GB font's own do
yOffset = def.ttf.yOffset or (obj.getBaseline
and (GLYPH - 1 - obj:getBaseline()) or (GLYPH - obj:getHeight())),
-- Single characters that keep their ROM tile instead of coming from
-- the TTF. A CJK translation sizes the font so a kana fills the 8px
-- cell, which leaves Latin narrower than the tile font it replaces:
-- the numbers in a right-aligned column (the party menu's ":L12" over
-- "34/ 34") then no longer land where the 8px-per-character layout put
-- them. Naming "0123456789" here keeps digits on the vanilla tiles --
-- identical to the English build -- while kana still come from the
-- font. Sequence keys, so "é" or a "<PK>" macro can be listed too.
tiles = tileSet(def.ttf.tiles),
widths = {}, chars = {},
}
else
@@ -227,9 +264,10 @@ function Font.split(text)
for _, entry in ipairs(candidates) do
local len = #entry.seq
if text:sub(i, i + len - 1) == entry.seq then
if ttf then
if ttf and not ttf.tiles[entry.seq] then
-- single characters belong to the TTF; only multi-character
-- sequences (ligatures, <PK> macros) keep their tile mapping
-- sequences (ligatures, <PK> macros) keep their tile mapping,
-- plus anything the mod named in ttf.tiles (see Font.load)
local cp, last = utf8Decode(entry.seq, 1)
if cp and last == len then break end
end
+29
View File
@@ -53,6 +53,28 @@ T.eq(#Font.encode("'d"), 1, "and still consumes the whole sequence")
T.eq(Font.encode("\227\129\130")[1], BASE + 0x3042,
"kana with no charmap entry at all still gets a glyph")
-- ------------------------------------------------- ttf.tiles opts back out
-- A CJK translation sizes the font so a kana fills the 8px cell, which leaves
-- Latin narrower than the tile font it replaces and pulls the numeric columns
-- (the party menu's ":L12" over "34/ 34") out of line. ttf.tiles names the
-- characters that keep their ROM tile anyway, so numbers stay identical to the
-- English build while kana still come from the font.
Font.load({ font = { charmap = CHARMAP, ttf = { tiles = "A" } } })
T.eq(Font.encode("A")[1], 0x80, "a character in ttf.tiles keeps its tile glyph")
T.eq(Font.advanceOf(0x80), 8, "and its 8px monospace advance with it")
T.eq(Font.encode("\195\169")[1], BASE + 0xE9,
"while everything else still routes to the TTF")
-- a list form, for naming a multi-character sequence explicitly
Font.load({ font = { charmap = CHARMAP, ttf = { tiles = { "\195\169" } } } })
T.eq(Font.encode("\195\169")[1], 0xBA,
"the list form accepts a multi-byte character")
T.eq(Font.encode("A")[1], BASE + 65, "and leaves the others on the TTF")
Font.load({ font = { charmap = CHARMAP, ttf = {} } })
T.eq(Font.encode("A")[1], BASE + 65, "no tiles list means the TTF takes it back")
-- metrics come straight from the font object (the stub: half the point
-- size per codepoint, doubled from U+1000 up, mimicking Plain Pixel's
-- single/double width split)
@@ -161,6 +183,13 @@ do
T.check(select(1, Schemas.check(spec, "font", "ttf",
{ file = "mods/x/f.ttf", size = 11, spacing = 1, yOffset = -3 })) == true,
"so is a fully tuned entry")
T.check(select(1, Schemas.check(spec, "font", "ttf",
{ size = 10, tiles = "0123456789/:" })) == true,
"tiles takes a string of characters")
T.check(select(1, Schemas.check(spec, "font", "ttf",
{ tiles = { "0", "<PK>" } })) == true, "or a list of charmap sequences")
T.check(Schemas.check(spec, "font", "ttf", { tiles = 10 }) == nil,
"but not a number")
T.check(Schemas.check(spec, "font", "ttf", { image = "x.png", base = 0x100 })
== nil, "a page payload under the ttf id is refused")
T.check(Schemas.check(spec, "font", "page", {}) == nil,