-- 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.load_texture("global/nameplate/shadow") self.ai = tex.load_texture("global/nameplate/ai") self.outline = tex.load_texture("global/nameplate/outline") self.frame_top = tex.load_texture("global/nameplate/frame_top") self.frame_top_rainbow = tex.load_texture("global/nameplate/frame_top_rainbow") self.dan_emblem_bg = tex.load_texture("global/nameplate/dan_emblem_bg") self.dan_emblem = tex.load_texture("global/nameplate/dan_emblem") self.dan_emblem_gold = tex.load_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 `) 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(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 `) 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_id("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