mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Merge branch 'dev' of https://github.com/bryanthaboi/gen1recomp into dev
This commit is contained in:
+18
-2
@@ -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.
|
||||
|
||||
@@ -471,6 +480,13 @@ automatically, so the tab opens on an "Add an index" prompt until you name
|
||||
one; paste an index URL or its `owner/repo` and it is remembered in
|
||||
`options.lua`. More than one index can be added, and the listings merge.
|
||||
|
||||
A feed author can publish per-mod release stats by adding three optional
|
||||
fields to an entry -- `downloads` (total across every release), and
|
||||
`first_release` / `last_release` (ISO days) -- which the listing shows in
|
||||
the same gold line the MODS tab uses. The fields are additive: feeds that
|
||||
carry them stay readable by every build that predates them, and feeds that
|
||||
do not render exactly as before.
|
||||
|
||||
## Soft reset (all versions)
|
||||
|
||||
Holding A, B, START and SELECT together restarts the game the way flicking
|
||||
|
||||
@@ -1243,13 +1243,9 @@ local function buildModsPanel(imp, parent, m)
|
||||
-- counts, so a pre-downloads cache entry costs the line, not a wrong "0".
|
||||
local dlLine
|
||||
if info and info.downloads then
|
||||
local formatted = ModUpdate.formatCount(info.downloads.total)
|
||||
if info.dates then
|
||||
dlLine = Strings("%s downloads across all releases - Released %s - Updated %s",
|
||||
formatted, info.dates.first, info.dates.latest)
|
||||
else
|
||||
dlLine = Strings("%s downloads across all releases", formatted)
|
||||
end
|
||||
local d = info.dates
|
||||
dlLine = ModUpdate.statsLine(info.downloads.total,
|
||||
d and d.first, d and d.latest)
|
||||
end
|
||||
|
||||
-- measure the body: name (with the badge beside it only when it fits),
|
||||
@@ -1524,9 +1520,17 @@ local function buildFindPanel(imp, parent, m)
|
||||
local btnH = math.ceil(textHeight(chipSize)) + 14
|
||||
for _, entry in ipairs(rows) do
|
||||
local action, note = findActionFor(entry, installed[entry.id])
|
||||
-- Feed-published release stats (downloads, first/last release date) in
|
||||
-- the same gold line the MODS tab uses; absent until a feed carries them.
|
||||
local statsLine
|
||||
if entry.downloads ~= nil or entry.first_release or entry.last_release then
|
||||
statsLine = ModUpdate.statsLine(entry.downloads,
|
||||
entry.first_release, entry.last_release)
|
||||
end
|
||||
|
||||
local bodyH = math.ceil(textHeight(titleSize))
|
||||
+ 4 + math.ceil(textHeight(smallSize))
|
||||
if statsLine then bodyH = bodyH + 4 + wrapHeight(smallSize, statsLine, bodyW) end
|
||||
if note then bodyH = bodyH + 4 + wrapHeight(smallSize, note, bodyW) end
|
||||
if entry.summary and entry.summary ~= "" then
|
||||
bodyH = bodyH + 4 + wrapHeight(smallSize, entry.summary, bodyW)
|
||||
@@ -1571,6 +1575,9 @@ local function buildFindPanel(imp, parent, m)
|
||||
end
|
||||
label(body, meta, smallSize, C("detail"),
|
||||
{ width = "100%", textWrap = false, textOverflow = "ellipsis" })
|
||||
if statsLine then
|
||||
label(body, statsLine, smallSize, C("gold"), { width = "100%" })
|
||||
end
|
||||
if note then label(body, note, smallSize, C("green"), { width = "100%" }) end
|
||||
if entry.summary and entry.summary ~= "" then
|
||||
label(body, entry.summary, smallSize, C("detail"), { width = "100%" })
|
||||
|
||||
@@ -174,6 +174,13 @@ local function parseEntry(raw)
|
||||
conflicts = raw.conflicts,
|
||||
thumbnail = str(raw.thumbnail),
|
||||
description_url = str(raw.description_url),
|
||||
-- Optional release stats a feed author can publish: total downloads
|
||||
-- across all releases plus first/last release dates. Additive-only,
|
||||
-- so a feed that carries them stays readable by every build that
|
||||
-- predates them (and one that does not still renders fine here).
|
||||
downloads = tonumber(raw.downloads),
|
||||
first_release = str(raw.first_release),
|
||||
last_release = str(raw.last_release),
|
||||
latest = parseLatest(raw.latest),
|
||||
update_check = str(raw.update_check) or "pending",
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ local ModUpdate = {}
|
||||
|
||||
ModUpdate.CACHE_TTL = 6 * 60 * 60 -- six hours
|
||||
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
local function stripV(tag)
|
||||
return (tostring(tag):gsub("^[vV]", ""))
|
||||
end
|
||||
@@ -237,6 +239,24 @@ function ModUpdate.formatCount(n)
|
||||
return (s:gsub("^,", ""))
|
||||
end
|
||||
|
||||
-- The one-line stats string both launcher panels show: "1,234 downloads
|
||||
-- across all releases - Released 2024-05-31 - Updated 2026-07-01".
|
||||
-- Each part is optional; nil everywhere means nil, so a row with no data
|
||||
-- shows no line rather than a wrong "0".
|
||||
function ModUpdate.statsLine(total, first, latest)
|
||||
local parts = {}
|
||||
if total ~= nil then
|
||||
parts[#parts + 1] = Strings("%s downloads across all releases",
|
||||
ModUpdate.formatCount(total))
|
||||
end
|
||||
if first or latest then
|
||||
parts[#parts + 1] = Strings("Released %s - Updated %s",
|
||||
first or "?", latest or "?")
|
||||
end
|
||||
if #parts == 0 then return nil end
|
||||
return table.concat(parts, " - ")
|
||||
end
|
||||
|
||||
-- ------- cache (options.modUpdateCache[repo])
|
||||
|
||||
local function cacheStore()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -110,6 +110,27 @@ do
|
||||
"the release asset URL survives parsing")
|
||||
eq(m.permissions[1], "engine_internals", "permissions are kept")
|
||||
eq(m.update_check, "ok", "update_check is kept")
|
||||
check(m.downloads == nil and m.first_release == nil and m.last_release == nil,
|
||||
"a feed without release stats parses them as absent")
|
||||
end
|
||||
|
||||
-- release stats a feed can publish: total downloads and first/last dates
|
||||
-- ride along additively, so a feed carrying them stays readable by every
|
||||
-- build that predates them
|
||||
do
|
||||
local withStats = {}
|
||||
for k, v in pairs(NUZLOCKE) do withStats[k] = v end
|
||||
withStats.downloads = 1234
|
||||
withStats.first_release = "2024-05-31"
|
||||
withStats.last_release = "2026-07-01"
|
||||
local index = ModIndex.parse(feed({ withStats }))
|
||||
local m = index.mods[1]
|
||||
eq(m.downloads, 1234, "total downloads are kept")
|
||||
eq(m.first_release, "2024-05-31", "first release date is kept")
|
||||
eq(m.last_release, "2026-07-01", "last release date is kept")
|
||||
withStats.downloads = "9999"
|
||||
m = ModIndex.parse(feed({ withStats })).mods[1]
|
||||
eq(m.downloads, 9999, "numeric-string downloads are coerced")
|
||||
end
|
||||
|
||||
-- schema_version is a contract, not a hint: an unknown one is refused rather
|
||||
|
||||
@@ -183,6 +183,22 @@ eq(ModUpdate.formatCount("12345"), "12,345", "numeric strings are accepted")
|
||||
eq(ModUpdate.formatCount(nil), "0", "nil formats as zero")
|
||||
eq(ModUpdate.formatCount("garbage"), "0", "garbage formats as zero")
|
||||
|
||||
-- statsLine: the shared launcher row line, part by part
|
||||
eq(ModUpdate.statsLine(1234567, "2024-05-31", "2026-07-01"),
|
||||
"1,234,567 downloads across all releases - Released 2024-05-31 - Updated 2026-07-01",
|
||||
"full stats line")
|
||||
eq(ModUpdate.statsLine(82, nil, nil),
|
||||
"82 downloads across all releases",
|
||||
"downloads alone")
|
||||
eq(ModUpdate.statsLine(nil, "2024-05-31", "2026-07-01"),
|
||||
"Released 2024-05-31 - Updated 2026-07-01",
|
||||
"dates alone")
|
||||
eq(ModUpdate.statsLine(0, nil, nil),
|
||||
"0 downloads across all releases",
|
||||
"a real zero still shows")
|
||||
check(ModUpdate.statsLine(nil, nil, nil) == nil,
|
||||
"no data at all means no line")
|
||||
|
||||
-- cacheUsable: a cache entry from before downloads existed must not be
|
||||
-- trusted, everything current is
|
||||
do
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user