diff --git a/docs/new-features.md b/docs/new-features.md index c3c901e3..04658eff 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -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. diff --git a/src/mods/Schemas.lua b/src/mods/Schemas.lua index ebd1e7f6..246ead7a 100644 --- a/src/mods/Schemas.lua +++ b/src/mods/Schemas.lua @@ -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" diff --git a/src/render/Font.lua b/src/render/Font.lua index 7015a3fc..926b41e5 100644 --- a/src/render/Font.lua +++ b/src/render/Font.lua @@ -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", "" }) 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 "" 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, macros) keep their tile mapping + -- sequences (ligatures, 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 diff --git a/tests/engine/ttf_font_mode.lua b/tests/engine/ttf_font_mode.lua index 3f4a4219..94e14aa3 100644 --- a/tests/engine/ttf_font_mode.lua +++ b/tests/engine/ttf_font_mode.lua @@ -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", "" } })) == 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,