Files
YataiDONNijiiro/Scripts/global/nameplate.lua
T
2026-09-20 13:37:39 -04:00

247 lines
14 KiB
Lua

-- YataiDON-HSS override of PyTaikoGreen/global/nameplate.lua
--
-- ROUND 15 (agent r15-audit-global) -- REBUILT against `common/name_plate.nulm`.
-- Sources for every number below:
-- * `common/NamePlate.lua` (the reference build script) SetPlayerInfoPlayData -- which frame LABEL is used,
-- * `common/name_plate.nulm` MC 280 ("main", 100 frames, 20 labels) -- the display list per label,
-- * its DefineEditText records -- the text boxes, font sizes, fill colours and border widths,
-- * `movie/attractdemo_cn_tw.wmv` frame 1 -- the cabinet's own render of the coin plate, which is
-- what settled the name's colour (see "Name colour" below).
--
-- ## Canvas mapping
-- MC 280 is registered at the plate CENTRE. Our textures live on a 408x96 canvas whose plate art
-- sits at x 32..376 (round 6), so
-- our canvas px = MC-280-local + (204, 48)
-- Check: badge shape139 is 88x88 at local (-168,-44) -> canvas (36,4) == 1p.png's texture.json
-- entry; dan_emblem_bg shape243 is 160x48 at local (-160,-4) -> canvas (44,44) == its entry.
--
-- ## The labels the cabinet actually uses (MC 280 label -> frame)
-- 1p_coin 0 | 1p 5 | 1p_dani 10 | 1p_shougou 15 | 1p_shougou_dani 20 (2p_* = +25, newentry 50, ai 70)
-- SetPlayerInfoPlayData picks Np_coin for `user_type == kCoin` and -- this is the part the port got
-- wrong -- appends `_shougou` / `_dani` ONLY in the non-coin branch. A cabinet plate is therefore
-- NEVER "coin plate + dan chip": that combination does not exist. We drew it, which is the
-- squashed dark chip over the 1P badge the user reported.
--
-- ## Display list per label (MC-280-local, before the +(204,48))
-- d3 shape69 white plate 344x96 @ (-172,-48) every label
-- d4 shape238 title band 336x40 @ (-168,-40) every label EXCEPT *_coin
-- d5 shape243 dan chip plate 160x48 @ (-160, -4) *_dani only
-- d6 shape240 empty-band fill 320x40 @ (-160,-40) Np / newentry only (no title set)
-- d8 shape139/248 player badge 88x88 @ (-168,-44) every label except newentry
-- d11 MC232 `dani` (the disk + kanji) @ (-62,16) *_dani only
-- d10 text_title, d12 text_name see the box table
--
-- ## Text boxes (rect from the ET record, converted to canvas px)
-- label text_name box centre font fill border
-- 1p_coin / 2p_coin (131,23)-(321,83) (226, 53) 30 #ffffff 3
-- 1p / 1p_shougou (131,37.5)-(321,97.5) (226, 67.5) 24 #ffffff 3
-- 1p_dani / *_dani (166,37.5)-(356,97.5) (261, 67.5) 24 #ffffff 3
-- newentry (109,37.5)-(299,97.5) (204, 67.5) 24 #ffffff 3
-- text_title (band) (119,4.5)-(334,64.5) (226.5, 34.5) 22 #000000 0
-- Lumen dynamic text is always vertically centred in its rect (FontImpl::GetAlignOffsetY,
-- research/lumen_text.md 3a) and align byte 2 = centred horizontally, so the box centre IS the
-- ink centre. raylib's MeasureTextEx adds the glyph spacing after the last glyph too, so an
-- OutlinedText's ink centre is (width - spacing)/2 -- applied below.
--
-- ## Name colour -- the one thing the record alone could not settle
-- Every text_name ET is color="#ffffff" with border 3, but the border COLOUR is not in the record
-- (`+50` is zero corpus-wide, so the renderer substitutes its own default -- research/lumen_text.md
-- section 2). Rounds 4-6 guessed the fill was dark and dropped the border.
-- `movie/attractdemo_cn_tw.wmv` frame 1 shows the cabinet's own 1P coin plate with the name
-- rendered as WHITE letters with a BLACK outline on the white pill, which matches the record's
-- fill exactly and pins the border colour to black. So: white fill, black outline 3.
-- text_title is #000000 with border 0 -- black, no outline, unchanged.
local ENGINE_DEFAULT_TITLE = "Donder Debut!"
local TEXT_SPACING = 2.0
-- ROUND 62: see the long note in `Nameplate.new`. The dan ladder is 25 ranks
-- (0=初級 .. 24=達人) -- `Graphics/global/nameplate/texture.json`'s 25 `dan_emblem`
-- crops and `src/scenes/dan_select.cpp`'s `dan_index > 24` range check agree.
local DAN_FRAME_COUNT = 25
-- `frame_top` bakes the band + the 5 title backgrounds (texture.json: 5 crops), which
-- is exactly the 5 values `title_bg_1p`/`title_bg_2p` offer in the settings menu.
local TITLE_BG_FRAMES = 5
-- name boxes, canvas px: {centre_x, centre_y, font}
local NAME_COIN = { 226, 53, 30 } -- 1p_coin / 2p_coin
local NAME_COIN_BARE = { 204, 53, 30 } -- no badge (YataiDON's PLAYER_NUM_ALL; no cabinet label)
local NAME_BAND = { 226, 67.5, 24 } -- 1p / 1p_shougou
local NAME_BAND_DANI = { 261, 67.5, 24 } -- 1p_dani / 1p_shougou_dani
local NAME_BAND_BARE = { 204, 67.5, 24 } -- newentry
local NAME_BOX_W = 190 -- every text_name rect is 190 wide
local TITLE_CX, TITLE_CY, TITLE_FONT, TITLE_BOX_W = 226.5, 34.5, 22, 215
Nameplate = {}
Nameplate.__index = Nameplate
local PLAYER_NUM_AI = 5
local PLAYER_NUM_ALL = 0
function Nameplate.new(name, title, player_num, dan, is_gold, is_rainbow, title_bg)
local self = setmetatable({}, Nameplate)
self.player_num = player_num
self.dan = dan
self.is_gold = is_gold
self.is_rainbow = is_rainbow
self.title_bg = title_bg
-- "Donder Debut!" is the scores.db default for a fresh player, i.e. "no title set" for us
-- (round 4). The cabinet's own rule is playData.user_data.title ~= "".
self.has_title = (title ~= nil and title ~= "" and title ~= ENGINE_DEFAULT_TITLE)
self.shadow = tex.get_texture("global/nameplate/shadow")
self.ai = tex.get_texture("global/nameplate/ai")
self.outline = tex.get_texture("global/nameplate/outline")
self.frame_top = tex.get_texture("global/nameplate/frame_top")
self.frame_top_rainbow = tex.get_texture("global/nameplate/frame_top_rainbow")
self.dan_emblem_bg = tex.get_texture("global/nameplate/dan_emblem_bg")
self.dan_emblem = tex.get_texture("global/nameplate/dan_emblem")
self.dan_emblem_gold = tex.get_texture("global/nameplate/dan_emblem_gold")
-- ROUND 15: `dan` must be a real frame index into `dan_emblem`, not merely "not -1".
-- Booting straight into a chart (`YataiDON.exe <tja> <diff>`) hands over a PlayerData that
-- never went through ENTRY, and its `dan` came through as **69** against a 25-frame texture:
-- `draw_texture` then threw `vector::_M_range_check` once per frame, forever. Clamp to the
-- texture's own frame count and treat anything outside it as "no dan", which is also the
-- cabinet's condition (`daniRecordInfo.now_dani > 0` gates the whole `_dani` suffix).
--
-- ROUND 62 (r62-nameplate-fix): the clamp above was correct in intent and WRONG in
-- source. `dan_emblem` is a `crop` SHEET in `Graphics/global/nameplate/texture.json`
-- (25 rectangles, 72x40 each), not a multi-file `FramedTexture`, and
-- `get_texture_info().frame_count` is computed as
-- `dynamic_cast<FramedTexture*>(tex_obj)` ? textures.size() : 1
-- (`src/libs/script.cpp` get_texture_info) -- it does not look at `crop_data` at all,
-- so for this texture it reports **1**. `dan < 1` therefore rejected every rank
-- except 初級 (0): `has_dan` was false for dan 1..24 on EVERY screen, so the dan chip
-- (`dan_emblem_bg` + `dan_emblem`) was never drawn and the name box never shifted from
-- (226,67.5) to the *_dani (261,67.5) box. Measured live this round with a probe on
-- the shipped player row (dan=24): `frames=1 has_dan=false`.
-- This is the exact trap `Scripts/entry/entry.lua:129-134` already documents for
-- `indicator/background` ("the bake is a `crop` list ... `get_texture_info` only
-- reports a real count for `FramedTexture` -- for this one it returns 1, so a
-- `frame_count >= 325` guard silently drew nothing"); ROUND 15 wrote this guard before
-- that note existed and fell into the same hole.
-- The count is a fixed arcade quantity, not a skin variable: the ladder is 25 ranks,
-- 0=初級 .. 24=達人, which is texture.json's 25 crops AND the engine's own course
-- range check (`dan_select.cpp`: `if (dan_index < 0 || dan_index > 24) dan_index = -1`).
-- `frame_count` is still honoured when it is meaningful (>1), so a skin that ever
-- re-bakes the sheet as real frames keeps working.
-- ROUND 62 also fixed the binding itself (`get_texture_info` now maxes in
-- `crop_data->size()`, ENGINE_BINDINGS.md ROUND 62): on a current engine `reported`
-- is 25 and the constant is never reached. Verified live after the rebuild:
-- `dan_emblem.frame_count=25 indicator.background.frame_count=325 has_dan=true`.
-- The constant stays as the belt-and-braces path for a pre-R62 binary.
local emblem_info = tex.get_texture_info("nameplate", "dan_emblem")
local reported = (emblem_info and emblem_info.frame_count) or 0
local dan_frames = (reported > 1) and reported or DAN_FRAME_COUNT
-- R15's fail-soft is preserved: booting straight into a chart
-- (`YataiDON.exe <tja> <diff>`) once handed over dan = 69, and an unclamped frame
-- index threw `vector::_M_range_check` once per frame, forever.
self.has_dan = (type(dan) == "number" and dan >= 0 and dan < dan_frames)
-- Np_coin == no title AND no dan. Anything else is the band family, exactly as
-- SetPlayerInfoPlayData builds Np[_shougou][_dani].
self.is_coin = not self.has_title and not self.has_dan
self.has_badge = (player_num ~= PLAYER_NUM_ALL and player_num ~= PLAYER_NUM_AI)
self.rainbow_anim = tex.get_animation(12, "global")
local box = self:name_box()
-- white fill + black outline 3 (see the header); the cabinet never shrinks the font, it
-- squeezes the glyphs horizontally inside the box, which is what the x2 override does below.
-- OutlinedText multiplies the thickness by screen_scale (1.5 here) and the record's border
-- is already in 1080p stage px, so 3/1.5 = 2 draws the cabinet's 3.
self.name_text = text.create_raw_text(name or "", box[3], { 255, 255, 255, 255 }, { 0, 0, 0, 255 },
false, 2, TEXT_SPACING)
self.title_text = text.create_raw_text(title or "", TITLE_FONT, { 0, 0, 0, 255 }, { 0, 0, 0, 0 },
false, 0, TEXT_SPACING)
if self.has_badge then
local pnum_key = tostring(player_num) .. "p"
self.pnum_tex = tex.get_texture("global/nameplate/" .. pnum_key)
end
return self
end
-- which text_name box this plate uses, per the label SetPlayerInfoPlayData would pick
function Nameplate:name_box()
if self.is_coin then
return self.has_badge and NAME_COIN or NAME_COIN_BARE
end
if not self.has_badge then return NAME_BAND_BARE end
return self.has_dan and NAME_BAND_DANI or NAME_BAND
end
-- one centred, whole-pixel-snapped OutlinedText draw inside a Lumen EditText box.
-- ROUND 11 (text sharpness): an OutlinedText is a rasterised texture drawn 1:1 with BILINEAR,
-- so a .5 destination blends every texel 50/50 -- snap x, y and the width.
local function draw_in_box(t, x, y, cx, cy, box_w, fade)
if not t or t.width == nil then return end
local w = math.floor(math.min(t.width, box_w) + 0.5)
local dx = math.floor(x + cx - (w - TEXT_SPACING) / 2.0 + 0.5)
local dy = math.floor(y + cy - t.height / 2 + 0.5)
t:draw({ x = dx, y = dy, x2 = w - t.width, fade = fade })
end
function Nameplate:update(current_ms)
self.rainbow_anim:update(current_ms)
end
function Nameplate:draw(x, y, fade)
fade = fade or 1.0
if self.player_num == PLAYER_NUM_AI then
tex.draw_texture(self.ai, { x = x, y = y })
return
end
-- d3 shape69: the white plate, present in every label
tex.draw_texture(self.shadow, { x = x, y = y, fade = fade })
if not self.is_coin then
-- d4 shape238 + d7 title_instance: the title band. Our frame_top bakes the band and the
-- four title backgrounds into one 5-frame texture; the cabinet's plain shape240 fill
-- (the band with no title set) is not baked, so a dan-only plate borrows title_bg's colour.
-- ROUND 62: clamp, for the same fail-soft reason the dan index is clamped.
-- `title_bg` arrives straight from the `players.title_bg` column with no engine-side
-- validation (`config_ref.h` `nameplate_1p/title_bg` writes it raw), and an
-- out-of-range frame throws `vector::_M_range_check` once per frame forever
-- (`texture.cpp` draw path re-throws after logging) -- the dan=69 failure mode.
local frame = (self.player_num == PLAYER_NUM_ALL) and 2 or self.title_bg
if type(frame) ~= "number" or frame < 0 or frame >= TITLE_BG_FRAMES then frame = 0 end
if self.is_rainbow then
local ra = self.rainbow_anim.attribute
if ra > 0 and ra < 6 then
tex.draw_texture(self.frame_top_rainbow, { frame = math.floor(ra) - 1, x = x, y = y, fade = fade })
end
tex.draw_texture(self.frame_top_rainbow, { frame = math.floor(ra), x = x, y = y, fade = fade })
else
tex.draw_texture(self.frame_top, { frame = frame, x = x, y = y, fade = fade })
end
tex.draw_texture(self.outline, { x = x, y = y, fade = fade })
end
-- d5 shape243 + d11 MC232: the dan chip. *_dani labels only, and those only exist in the
-- band family -- drawing it on the coin plate is what squashed the badge (see the header).
if self.has_dan and not self.is_coin then
tex.draw_texture(self.dan_emblem_bg, { x = x, y = y, fade = fade })
local emblem = self.is_gold and self.dan_emblem_gold or self.dan_emblem
tex.draw_texture(emblem, { frame = self.dan, x = x, y = y, fade = fade })
end
-- d8 shape139 / shape248 + d9 plate_player_no
if self.has_badge then
tex.draw_texture(self.pnum_tex, { x = x, y = y, fade = fade })
end
-- d10 text_title -- band family only; Np_coin clears the title outright
-- (SetPlayerInfoPlayData: if plate_label == playerSide .. "p_coin" then userTitle = "" end)
if not self.is_coin and self.has_title then
draw_in_box(self.title_text, x, y, TITLE_CX, TITLE_CY, TITLE_BOX_W, fade)
end
-- d12 text_name
local box = self:name_box()
draw_in_box(self.name_text, x, y, box[1], box[2], NAME_BOX_W, fade)
end