-- YataiDON-HSS override of PyTaikoGreen/global/coin_overlay.lua -- Arcade (the reference build) global chrome drawn on every non-game screen: -- * common/credit : 「フリープレイ」 text, centred (960,1048), skin_config free_play font 34 (see draw) -- * common/qr__check : QR status chip (106x106 @1570,38) — label ng/ok; YataiDON has no QR -> always NG (frame 0) -- * common/credit_player_side : 2P cloud bubble 「2人プレイ / 太鼓をたたいてスタート!」 (416x280 @1492,638), -- shown while the 2P side is not entered (player_num == 1). Blinks in the arcade: 3 s loop, 2 s solid (cloud 0.70/text 1.0) + 0.5 s out + 0.5 s in (see draw). CoinOverlay = {} CoinOverlay.__index = CoinOverlay -- ROUND 8 — the 2P invite cloud's blink is no longer the three hand-written branches -- round 6 fitted to it: `Scripts/anim/credit_side.lua` is exported straight out of -- `common/credit_player_side.nulm` sprite 10 (the 180-frame MC the root parks on) by -- scratchpad/anim_export.py. See ANIM_PIPELINE.md. The export CONFIRMED round 6's -- model exactly -- cloud `#3@0` 179/256 -> 0 over f0..f30, back over f30..f59, steady -- f59..f179, and the text `credit_all_instance` the same shape at 256/256 -- so this is -- provenance and a re-export path, not a visible fix. The one thing that stays a -- constant is the 8-frame pop-in (root sprite 11 label `main` f9: +32/256 per frame, -- exactly linear), because a two-row table for a straight line buys nothing. -- ROUND 8: fail soft. `pcall` the require and use `anim.safe`, which hands back a NULL -- clip when a generated table is missing or malformed, so every reader below falls -- through to its `or ` default instead of taking the screen down. local _ca_ok, cs_anim = pcall(require, "anim/sampler") if not _ca_ok then cs_anim = { safe = function() -- sampler.lua itself is missing: hand back the same NULL clip shape it would local n = {} n.at = function() return nil end; n.get = function() return nil end n.label = function() return nil end; n.has = function() return false end n.first = function() return 0 end; n.last = function() return 0 end return n end } end local CS = cs_anim.safe("anim/credit_side") local CS_LEN = math.max(1, CS:last() - CS:first() + 1) -- 180 frames = 3 s (1 if the table is gone) local CS_POP_F = 8 -- verified against sprite 11 f9..f17 function CoinOverlay.new() local self = setmetatable({}, CoinOverlay) -- outline 5 = the arcade record's `border` (ET4 +68); ROUND 12. -- ROUND 15: 5 was the record's `border` passed raw; OutlinedText scales it by 1.5, so it -- drew 7.5 px. 3 gives 4.5 px, the closest integer to the cabinet's 5. self.text = text.create_text("free_play", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 3, 5) self.config = tex.get_skin_config("free_play") -- texture names are fixed by the engine's generated TexID table, so arcade art is -- stored under existing names: banapass_osaifu_keitai = QR chip (0 NG / 1 OK), -- camera = 2P cloud bubble (both frames identical) self.qr = tex.load_texture("global/overlay/banapass_osaifu_keitai") self.bubble = tex.load_texture("global/overlay/camera") -- ROUND 15 (r15-audit-global): both lines are `credit_player_side.nulm` DefineEditText records -- ET7 (`text_player`) and ET5 (`text_credit`), and both say size **40**, border **6**, -- `color="#ffffff"` -- white fill with a black outline, not the dark-grey/no-outline pair -- round 6 guessed. (Border colour is not in the record -- research/lumen_text.md section 2 -- -- but `movie/attractdemo_cn_tw.wmv` frame 1 shows the same white-fill/black-border treatment -- on the name plate, which is the same renderer default.) See Graphics/global/MAPPING.md -- ROUND 15 items G3/G4. -- OutlinedText multiplies the thickness by screen_scale (`text.cpp:200`, -- `outline_thickness * global_tex.screen_scale`), which is 1.5 in this skin, while the -- record's `border` is already in 1080p stage px. Pass 6/1.5 = 4 to draw the arcade's 6. self.bubble_player = text.create_text("credit_side_player", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2) self.bubble_msg = text.create_text("credit_side_msg", { 255, 255, 255, 255 }, { 0, 0, 0, 255 }, false, 4, 2) self.bubble_cfg_p = tex.get_skin_config("credit_side_player") self.bubble_cfg_m = tex.get_skin_config("credit_side_msg") self.blink_t0 = tex.get_current_ms() return self end function CoinOverlay:update(current_ms) end -- common/credit: root places credit_text_instance at (960,1052.25) [absolute, this lumen's root -- sits at (0,0)]; `text_credit` = ET4, box **(760,991.75)-(1160,1087.75)** = 400x96, size 40, -- outline 5, align byte 2 (centre), font stylekey '0' -> size bucket "64" -> **jp_64**. -- ROUND 12: ROUND 4d sized this off the `face="MS 明朝"` string, which `lumen_fonts.md` §4c -- proves is inert Flash-authoring residue -- the cabinet has no vector font and picks its atlas -- from the record's style key alone. The real face is the heavy jp_64 display weight, so 34/3 was -- ~15 % too small. Arcade pen (lumen_text.md 3/3a, the same rule Graphics/result/MAPPING.md -- ROUND 6 uses): lineHeight = 92 * 40/64 = 57.5, pen.y = 991.75 + (96-57.5)/2 + 5 = 1016.0, and -- the katakana ink of 「フリープレイ」 then runs **1023.5..1061.6, centre 1042.56**. -- Our OutlinedText centres its whole image, whose katakana ink sits 3.0 px above that centre at -- size 40, so the centre that lands the ink on the cabinet's is 1042.56 + 3.0 = 1045.6 -> 1046. -- (The arcade's fill is also `border` = 5 px RIGHT of the box centre -- x 965, not 960 -- because -- FontImpl insets the fill by `border` on both axes; that 5 px is deliberately NOT copied here, -- see Graphics/loading/MAPPING.md ROUND 12 class C.) -- OutlinedText adds the skin_config x/y as an offset, so both are subtracted here. local FREE_PLAY_CY = 1046 -- ROUND 15: the two lines inside the 2P invite cloud, in stage px for the 2P seat -- (`credit_side_instance_2` at (1700,706)); the 1P seat mirrors x only. local BUBBLE_PLAYER_CY = 749 local BUBBLE_MSG_CY = 802 -- ROUND 16 (r16-select, QA defect #3 -- the CLIPPING half only; the alpha half was measured and -- dismissed, see the note at the bottom of draw()). -- The cabinet centres both lines on the seat (1700 / 220) inside EditText boxes that are WIDER -- than the screen allows on that side: ET7 `text_player` (1472,694)-(1928,790) = 456 wide and -- ET5 `text_credit` (1278,747)-(2122,843) = 844 wide, both centred on 1700. At that centre only -- 2*(1920-1700) = 440 px of a centred string can be on screen at all, and the cloud art itself is -- 416 px wide (`attract_credit_instance__shape3`, local (-208,-108)..(208,172)). -- The cabinet gets away with it because its own strings are short: ET5 is 「あと〇コイン入れてね!」 -- and ET7 「〇人プレイ」 (CreditSide.lua Setup swaps in `credit_rest_2p` / `credit_play_2p`). -- Our localised strings ("Hit the taiko to start!", 「太鼓をたたいてスタート!」) are far wider and ran -- off both screen edges. FIX: clamp the drawn width to the cloud and let OutlinedText squeeze the -- glyphs horizontally via `x2` -- the same shrink-to-fit `nameplate.lua` uses for the name box -- (`draw_in_box`), which is what the cabinet's own renderer does inside an EditText rect. -- DEVIATION, deliberate: the cabinet never squeezes these two lines because its JP strings fit. -- The squeeze only engages when a string is wider than the bubble, so JP/short strings are -- byte-for-byte unchanged; it is localisation headroom, not a re-layout. local BUBBLE_BOX_W = 416 -- cloud art width; also <= the 440 px a centred string can occupy at x=1700/220 -- Centred, whole-pixel-snapped OutlinedText draw clamped to `box_w` (see nameplate.lua draw_in_box). local function draw_bubble_line(t, cfg, cx, cy, fade) if not t or t.width == nil then return end local w = math.floor(math.min(t.width, BUBBLE_BOX_W) + 0.5) t:draw({ x = math.floor(cx - w / 2 - cfg.x + 0.5), y = math.floor(cy - t.height / 2 - cfg.y + 0.5), x2 = w - t.width, fade = fade }) end -- ROUND 73 (r73-danselect-condition-chrome), QA defect 1: 「free play在這畫面不展示」. -- -- The credit line used to be drawn UNCONDITIONALLY at the top of draw(), while -- the QR chip and the 2P invite cloud below it both carry explicit screen lists. -- That is ROUND 71's bug in the other direction: a global overlay member with no -- screen gate at all, so it leaked onto DAN_SELECT, where the cabinet shows none. -- -- What the cabinet actually is (the reference build, reference-sourced): the credit line is -- `AppAlwaysDisplayPartsCredit`, slot 5 of the `AppAlwaysDisplay` parts array -- (the only part carrying SetAttract/SetEntry/SetGame, vtable +48/+56/+64, which -- write mode 1/0/2 to this+36). Each scene picks its mode, and the per-scene -- table (`AppAlwaysDisplay::Scene*`, one folded function per preset) is: -- SceneTitle / SceneAttractMovie 0x140021FC0 -> +48 SetAttract -- SceneEntry 0x140022360 -> +56 SetEntry -- SceneResult 0x140022810 -> +56 SetEntry -- SceneSongSelect (+ AI variants) 0x140022490 -> +64 SetGame -- SceneDaniResult 0x140022940 -> +64 SetGame -- SceneEnsoGame 0x1400226F0 -> +24 FinishDisp (hidden) -- SceneDaniSelect has NO preset of its own: `SceneDaniSelect.obj.c` inlines the -- same calls and hits slot 5 with +64 (SetGame) then +8 (StartDisp). -- -- HONEST LIMIT, recorded rather than papered over: that table does NOT explain -- this defect. DAN_SELECT and SONG_SELECT take the identical branch (SetGame + -- StartDisp), yet the cabinet shows the caption on song select and not on dan -- select, so the real discriminator is somewhere below `SetCreditString`'s -- mode-2 arm (this+36 == 2, which then keys off the credit counter this+20 and -- IsFreePlay) and is NOT decoded. The list below is therefore the USER's direct -- side-by-side observation of the cabinet, written as a screen list so it has -- the same shape as the two gates underneath it and so the next round can -- extend it from evidence instead of unpicking a negation. local CREDIT_SCREENS = { ENTRY = true, PRACTICE_SELECT = true, AI_SELECT = true, TITLE = true, RESULT = true, -- DAN_SELECT: deliberately absent (QA defect 1). -- SONG_SELECT / SONG_SELECT_2P: absent -- the cabinet's song select shows no -- COINS / FREE PLAY line (user, side by side with the cabinet); RESULT keeps it. } function CoinOverlay:draw(x, y) local screen = tex.current_screen and tex.current_screen() or "" -- Fail soft: an unknown/blank screen name keeps the old always-on behaviour -- rather than silently blanking the credit line everywhere. local show_credit = (screen == "") or (CREDIT_SCREENS[screen] == true) if show_credit then -- ROUND 11 (text sharpness): whole-pixel snap — an OutlinedText is a rasterised -- texture drawn 1:1 with BILINEAR, so a .5 destination costs a full 50/50 texel blend. local text_x = math.floor(tex.get_screen_width() / 2 - self.text.width / 2 - self.config.x + 0.5) local text_y = math.floor(FREE_PLAY_CY - self.text.height / 2 - self.config.y + 0.5) self.text:draw({ x = text_x, y = text_y }) end -- ROUND 5: the engine re-draws this overlay on top of the song-loading (rainbow) transition -- with tex.in_transition() == true. The arcade loading screen shows ONLY the credit text -- (no QR chip, no 2P invite cloud), so stop here in that case. if tex.in_transition and tex.in_transition() then return end -- Anything a screen script wants drawn ABOVE the credit text (song_select.lua: the course-select -- drum guide pill, which the arcade layers over the credit line). Consumed and cleared each frame. if YD_DRAW_ABOVE_CREDIT then local fn = YD_DRAW_ABOVE_CREDIT YD_DRAW_ABOVE_CREDIT = nil fn() end -- ROUND 6 (item 46): the arcade title/attract screen carries no status row — the -- 段位道場 / 1プレイ4曲 / IC Card chips are entry_overlay.lua's and skin_config already -- parks them off-screen there (entry_overlay_title.x = -4000), but the QR chip is drawn -- from here, so it needs its own guard. -- The chip row belongs to the screens where a card can still be tapped: entry and the -- select screens. Title, the loading transition, result, game over and settings show none. -- ROUND 73: `screen` is now read once at the top of draw() (the credit gate). local chip_screens = (screen == "ENTRY" or screen == "SONG_SELECT" or screen == "SONG_SELECT_2P" or screen == "PRACTICE_SELECT" or screen == "AI_SELECT" or screen == "DAN_SELECT") if chip_screens and not (tex.in_transition and tex.in_transition()) then tex.draw_texture(self.qr, { frame = 0 }) end -- player_num: 1 = 1P alone -> invite on the 2P side (credit_side_instance_2 @1700,706), -- 2 = 2P alone -> invite on the 1P side (credit_side_instance_1 @220,706), 3 = both -> none -- The arcade only instantiates credit_side (CreditSide.lua) in entry and song select -- (entry_main.lua / song_select_all.lua); result, game over, dan, settings never show it. local scr = (screen ~= "") and screen or "ENTRY" local on_entry_or_select = (scr == "ENTRY" or scr == "SONG_SELECT" or scr == "SONG_SELECT_2P" or scr == "PRACTICE_SELECT" or scr == "AI_SELECT") -- ROUND 6 (2P): `global_data.player_num` is NEVER set to TWO_PLAYER(3) - scenes/entry.cpp -- forces it back to P1 once the second player joins, so tex.player_num() stays 1 for a -- 2P session and the invite cloud kept drawing on top of the 2P Don. The two signals the -- skin does get are the screen name (SONG_SELECT_2P) and entry.lua's draw_footer, which -- the engine hands p1_joined / p2_joined before it draws this overlay. local pn = tex.player_num() local both_joined = (pn == 3) or (scr == "SONG_SELECT_2P") or (scr == "ENTRY" and YD_ENTRY_BOTH_JOINED == true) -- ROUND 11 (entry credit screen): entry_main.lua turns `credit_side_[n]` visible only in -- ModeSelectStartWait -- never during CreditWait -- so the invite cloud must not draw -- while entry.lua still has the 1人プレイ/2人プレイ pills up (it refreshes -- YD_ENTRY_CREDIT_MS every frame it draws them, including the decide/fade-out tail). local credit_up = (scr == "ENTRY") and YD_ENTRY_CREDIT_MS ~= nil and (tex.get_current_ms() - YD_ENTRY_CREDIT_MS) < 100 -- ROUND 15 (mid-song-select 2P join): `song_select_all.lua` `CreditSideVisible` -- (l.1622) only turns `credit_side_[n]` visible while `playSongCount < 2`, the -- same window `Set2PEntryEnable` (l.1806) uses for the join itself. We used to -- advertise the cloud on every song select, including songs the engine now -- (correctly) refuses to join on. Entry is unconditional there (entry_main.lua -- ModeSelectStartWait keys only off is_use_credit), so gate the select screens. local join_window = (scr == "ENTRY") or not tex.songs_played or (tex.songs_played() < 2) if on_entry_or_select and join_window and not credit_up and not both_joined and (pn == 1 or pn == 2) then local side = (pn == 1) and 0 or 1 local cx_p = (pn == 1) and self.bubble_cfg_p.x or (tex.get_screen_width() - self.bubble_cfg_p.x) local cx_m = (pn == 1) and self.bubble_cfg_m.x or (tex.get_screen_width() - self.bubble_cfg_m.x) -- ROUND 8: sampled out of `anim/credit_side` (MC 10, 180 frames, no action -> loops -- forever): the cloud is track `#3@0` (placed at 179/256, so its own row already -- carries the 0.70) and the text is `credit_all_instance` (256/256). Visible 2 s, -- blinks away 1 s (fully invisible only on frame 30), 3 s period. local elapsed = tex.get_current_ms() - self.blink_t0 local f = (elapsed * 0.06) % CS_LEN -- arcade frame 0..179 local pop = math.min(1, elapsed * 0.06 / CS_POP_F) -- 133 ms pop-in on first show -- ROUND 16 (r16-select): DO NOT "fix" this to 1.0. QA round 16 reported the cloud as -- "far too transparent, you can read the stalls through it"; measured against the cabinet -- it is exactly right and opaque would be the regression. `common/credit_player_side.nulm` -- places shape 3 (the cloud, art fully opaque white) with color_mul [256,256,256,179] -- => 179/256 = 0.6992, which is the 0.6992 in `anim/credit_side`. `lum.py`'s render of the -- cabinet's own MC 10 peaks at alpha 178; ours draws 255 * 0.6992 = 178.3. The "ghost" -- reading is ENTRY-specific: 70 % white over the bright street art is low contrast by -- design, and the identical draw reads solid over song select's blue backdrop. local fade = (CS:get("#3@0", f, "a") or 0) * pop local fade_t = (CS:get("credit_all_instance", f, "a") or 0) * pop tex.draw_texture(self.bubble, { index = side, fade = fade }) -- OutlinedText adds the skin_config x/y as an offset to params.x/y, so subtract cfg.x/y again -- ROUND 11: whole-pixel snap (see draw() above) -- ROUND 15: y is now centred on the arcade's own ink centre instead of being read as a -- top-left out of skin_config. ET7 box (1472,694)-(1928,790) and ET5 box (1278,747)-(2122,843) -- for `credit_side_instance_2` at (1700,706); Lumen text is vertically centred in its rect, -- and the same lineHeight/border correction the free-play line uses (ROUND 12) puts the ink -- centres at 745.8 and 798.8, +3.0 for our katakana raster = 749 / 802. -- ROUND 16: clamped to the cloud so a long localised string cannot run off the screen edge -- (x=1920 on the 2P seat, x=0 on the 1P seat). No-op for a string narrower than the cloud. draw_bubble_line(self.bubble_player, self.bubble_cfg_p, cx_p, BUBBLE_PLAYER_CY, fade_t) draw_bubble_line(self.bubble_msg, self.bubble_cfg_m, cx_m, BUBBLE_MSG_CY, fade_t) end end