From 62cd701d61943e1bc13f1e396887e378df528924 Mon Sep 17 00:00:00 2001 From: BricOOtaku <90196012+BricOOtaku@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:28:55 +0100 Subject: [PATCH 1/4] Fix FT result layering for almost layers, reset UI on result init, and patch chance frame height --- src/game/game.cpp | 5 +++++ src/ui/result_ps4.cpp | 27 ++++++++++++++++++++++++++- src/ui/result_switch.cpp | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index 448abcd..01df18d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -479,4 +479,9 @@ void InstallGameHooks() // which normally would make the target effects aet stay on screen when retrying a song while // a link note is spawning (as we capture their aet handles) WRITE_MEMORY(0x14026E649, uint8_t, 0xE9, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0); + + // NOTE: Increase the height of chance_frame_bottom to hide the star tip + uint32_t ChanceFrmBtmHeight = 0x43150000; + WRITE_MEMORY(0x140274E47, uint32_t, ChanceFrmBtmHeight); + WRITE_MEMORY(0x140274E5B, uint32_t, ChanceFrmBtmHeight); } \ No newline at end of file diff --git a/src/ui/result_ps4.cpp b/src/ui/result_ps4.cpp index 089ab35..a6837a4 100644 --- a/src/ui/result_ps4.cpp +++ b/src/ui/result_ps4.cpp @@ -61,6 +61,25 @@ static std::string GetLocalizedLayerNameOnlyJP(std::string_view name) return std::string(name) + (GetGameLocale() == GameLocale_JP ? "_jp" : "_en"); } + +static void PatchWinAlmost(StageResultPS4* result, const char* name, int32_t flags = 0x20000) +{ + if (result->win_almost > 0 && nc::ShouldUseConsoleStyleWin()) + { + aet::Stop(&result->win_almost); + result->win_almost = aet::PlayLayer( + 1279, + 3, + flags, + name, + nullptr, + nullptr, + nullptr + ); + } +} + + class StyleBelt : AetElement { private: @@ -102,30 +121,36 @@ HOOK(void, __fastcall, PutScoreWindowIn, 0x1402365C0, StageResultPS4* result) { originalPutScoreWindowIn(result); SetWindowAet(result, GetLocalizedLayerNameOnlyJP("ps4_win_nc_in")); + PatchWinAlmost(result, "win_almost_in"); } HOOK(void, __fastcall, PutWinCount, 0x1402367C0, StageResultPS4* result) { originalPutWinCount(result); SetWindowAet(result, GetLocalizedLayerNameOnlyJP("ps4_win_nc_count")); + PatchWinAlmost(result, "win_almost_count"); } + HOOK(void, __fastcall, PutWinLoop, 0x140236A30, StageResultPS4* result) { originalPutWinLoop(result); SetWindowAet(result, GetLocalizedLayerNameOnlyJP("ps4_win_nc_loop"), true); + PatchWinAlmost(result, "win_almost_loop", 0x10000); } + HOOK(void, __fastcall, PutWinOut, 0x140236C80, StageResultPS4* result) { originalPutWinOut(result); SetWindowAet(result, GetLocalizedLayerNameOnlyJP("ps4_win_nc_out")); + PatchWinAlmost(result, "win_almost_out"); } HOOK(bool, __fastcall, StageResultPS4Init, 0x140231A70, StageResultPS4* result) { bool ret = originalStageResultPS4Init(result); - + state.ui.ResetAllLayers(); prj::string str; prj::string_view strv; aet::LoadAetSet(results::AetSetID, &str); diff --git a/src/ui/result_switch.cpp b/src/ui/result_switch.cpp index 5be618b..247f677 100644 --- a/src/ui/result_switch.cpp +++ b/src/ui/result_switch.cpp @@ -110,6 +110,7 @@ HOOK(void, __fastcall, CAetControllerGetLayout, 0x14065E200, CAetController* a1, HOOK(bool, __fastcall, StageResultSwitchInit, 0x14064C0E0, void* a1) { + state.ui.ResetAllLayers(); prj::string out; prj::string_view strv; aet::LoadAetSet(results::AetSetID, &out); From 1290696e282ebadf7ef4e8c534c314bf9a103733 Mon Sep 17 00:00:00 2001 From: BricOOtaku <90196012+BricOOtaku@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:01:04 +0100 Subject: [PATCH 2/4] Prevent overwriting the original challenge height by patching it only during Chance Time --- src/game/game.cpp | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/game/game.cpp b/src/game/game.cpp index 01df18d..f676298 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -30,6 +30,31 @@ struct NCSharedGameState static bool PlayNoteSoundEffectOnHit(PvGameTarget* target, TargetStateEx* ex); static bool CheckContinuousNoteSoundEffects(PvGameTarget* target, TargetStateEx* ex); + +float ChallengeTimeHeight = 0.0f; +constexpr float ChanceTimeHeight = 149.0f; +constexpr uintptr_t FrmBtmHeightAddrs[] = { 0x140274E47, 0x140274E5B }; + +void PatchFrmBtmHeight(float height) +{ + uint32_t bits; + std::memcpy(&bits, &height, sizeof(float)); + + for (uintptr_t addr : FrmBtmHeightAddrs) + WRITE_MEMORY(addr, uint32_t, bits); +} + +void SaveAndPatchCTHeight() +{ + float currentBtm = *reinterpret_cast(FrmBtmHeightAddrs[0]); + + if (currentBtm != ChanceTimeHeight) + ChallengeTimeHeight = currentBtm; + + PatchFrmBtmHeight(ChanceTimeHeight); +} + + HOOK(int32_t, __fastcall, GetHitStateInternal, 0x14026D2E0, PVGameArcade* game, PvGameTarget* target, @@ -418,7 +443,11 @@ HOOK(void, __fastcall, ExecuteModeSelect, 0x1503B04A0, PVGamePvData* pv_data, in { switch (mode) { + case ModeSelect_ChallengeStart: + PatchFrmBtmHeight(ChallengeTimeHeight); + break; case ModeSelect_ChanceStart: + SaveAndPatchCTHeight(); SetChanceTimeMode(&pv_data->pv_game->ui, ModeSelect_ChanceStart); break; case ModeSelect_ChanceEnd: @@ -479,9 +508,4 @@ void InstallGameHooks() // which normally would make the target effects aet stay on screen when retrying a song while // a link note is spawning (as we capture their aet handles) WRITE_MEMORY(0x14026E649, uint8_t, 0xE9, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0); - - // NOTE: Increase the height of chance_frame_bottom to hide the star tip - uint32_t ChanceFrmBtmHeight = 0x43150000; - WRITE_MEMORY(0x140274E47, uint32_t, ChanceFrmBtmHeight); - WRITE_MEMORY(0x140274E5B, uint32_t, ChanceFrmBtmHeight); -} \ No newline at end of file +} From e9b3ac13a91adc408e6eb62a573857adca982add Mon Sep 17 00:00:00 2001 From: BricOOtaku <90196012+BricOOtaku@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:12:37 +0100 Subject: [PATCH 3/4] Fix: initialize ChallengeTimeHeight with original FrmBtmHeight at startup --- src/game/game.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/game/game.cpp b/src/game/game.cpp index f676298..e91565c 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -508,4 +508,6 @@ void InstallGameHooks() // which normally would make the target effects aet stay on screen when retrying a song while // a link note is spawning (as we capture their aet handles) WRITE_MEMORY(0x14026E649, uint8_t, 0xE9, 0x7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0); + + ChallengeTimeHeight = *reinterpret_cast(FrmBtmHeightAddrs[0]); } From 45371279160a697903073ea9c3382924a8f46649 Mon Sep 17 00:00:00 2001 From: BricOOtaku <90196012+BricOOtaku@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:47:47 +0100 Subject: [PATCH 4/4] Add NC key input footer and localized menu --- src/diva.h | 5 +- src/ui/common.cpp | 37 +++++++++- src/ui/common.h | 55 +++++++++++++- src/ui/customize_sel.cpp | 155 +++++++++++++++++++++++++++++++++------ 4 files changed, 228 insertions(+), 24 deletions(-) diff --git a/src/diva.h b/src/diva.h index 7d1c60b..14ffb68 100644 --- a/src/diva.h +++ b/src/diva.h @@ -1261,4 +1261,7 @@ inline bool ShouldUpdateTargets() return !IsInSongResults() && !pv_game->paused && !pv_game->byte2; return false; -} \ No newline at end of file +} + +// BASED FROM: https://github.com/vixen256/ps4/blob/master/src/diva.cpp +inline FUNCTION_PTR(uint32_t*, __fastcall, GetSpriteId, 0x1405BC8F0, void* a1, prj::string_range* name); \ No newline at end of file diff --git a/src/ui/common.cpp b/src/ui/common.cpp index f70f151..eb1e321 100644 --- a/src/ui/common.cpp +++ b/src/ui/common.cpp @@ -411,4 +411,39 @@ std::string GetLanguageSuffix() { const char* suffixes[GameLocale_Max] = { "_jp", "_en", "_zh", "_tw", "_kr", "_fr", "_it", "_de", "_sp" }; return suffixes[GetGameLocale()]; -} \ No newline at end of file +} + +bool LoadSpriteByName(const char* name, uint32_t* out) +{ + prj::string_range range(name); + uint32_t* idPtr = GetSpriteId(nullptr, &range); + + if (!idPtr) + { + *out = 0; + return false; + } + + *out = *idPtr; + return true; +} + +bool LoadSpriteSet(const char* pattern, const char* suffix, uint32_t* out) +{ + auto name = util::Format(pattern, suffix); + return LoadSpriteByName(name.c_str(), out); +} + +bool LoadSpriteSetArray( const char* pattern, const int* indices, int count, uint32_t* out, const char* suffix ) +{ + for (int i = 0; i < count; i++) + { + int idx = indices ? indices[i] : (i + 1); + + auto name = util::Format(pattern, idx, suffix); + + LoadSpriteByName(name.c_str(), &out[i]); + } + + return true; +} diff --git a/src/ui/common.h b/src/ui/common.h index f396fff..844c5b1 100644 --- a/src/ui/common.h +++ b/src/ui/common.h @@ -204,4 +204,57 @@ diva::vec2 GetLayoutAdjustedPosition(const AetLayout& layout, std::string_view l void DrawSpriteAtLayout(const AetLayout& layout, std::string_view layer_name, uint32_t sprite_id, int32_t prio, int32_t res, bool adjust_pos = false); -std::string GetLanguageSuffix(); \ No newline at end of file +std::string GetLanguageSuffix(); + +bool LoadSpriteByName(const char* name, uint32_t* out); +bool LoadSpriteSet(const char* pattern, const char* suffix, uint32_t* out); +bool LoadSpriteSetArray(const char* pattern, const int* indices, int count, uint32_t* out, const char* suffix); + + +enum class InputType : int32_t { + XBOX = 0, + PLAYSTATION = 1, + SWITCH = 2, + STEAM = 3, + KEYBOARD = 4, + UNKNOWN = 5, +}; + +inline InputType NormalizeInputType(int32_t raw) +{ + switch (raw) + { + case 0: return InputType::XBOX; + case 1: return InputType::SWITCH; + case 2: return InputType::PLAYSTATION; + case 3: return InputType::STEAM; + case 4: return InputType::KEYBOARD; + default: return InputType::UNKNOWN; + } +} + +inline InputType GetInputType() +{ + void* state = diva::GetInputState(0); + if (!state) + return InputType::UNKNOWN; + + int32_t raw = *reinterpret_cast( + reinterpret_cast(state) + 0x2E8 + ); + + return NormalizeInputType(raw); +} + +inline const char* GetPlatformSuffix(InputType type) +{ + switch (type) + { + case InputType::KEYBOARD: return "_pc"; + case InputType::PLAYSTATION: return "_ps"; + case InputType::SWITCH: return "_sw"; + case InputType::STEAM: return "_st"; + case InputType::XBOX: return "_xb"; + default: return "_pc"; + } +} diff --git a/src/ui/customize_sel.cpp b/src/ui/customize_sel.cpp index 0a784cc..7ef65be 100644 --- a/src/ui/customize_sel.cpp +++ b/src/ui/customize_sel.cpp @@ -14,6 +14,10 @@ constexpr int32_t PreviewQueueIndex = 3; constexpr uint32_t SceneID = 14010150; +constexpr int32_t WindowPrio = 20; + +int32_t NCKeyInputFooter = 0; +static InputType previousInputType = InputType::UNKNOWN; struct CSStateConfigNC { @@ -36,6 +40,11 @@ struct SelectorExtraData std::vector sounds; }; +struct PlayCustomizeSelFooterArgs { + std::string footerName; + int32_t screen; +}; + static void PlayPreviewSoundEffect(HorizontalSelector* sel_base, const void* extra) { HorizontalSelectorMulti* sel = dynamic_cast(sel_base); @@ -117,31 +126,92 @@ class NCConfigWindow : public AetControl float win_opacity = 1.0f; ConfigSet* config_set; - static constexpr int32_t WindowPrio = 20; static constexpr int32_t MaxTabCount = 2; static constexpr uint32_t NumberSprites[2][3] = { { 3084111403, 965335902, 268427239 }, // FT UI { 880817216, 1732835926, 3315147794 } // MM+ UI }; + + uint32_t TabInfoSpritesFT[2]; + uint32_t TabInfoSpritesMM[2]; + + uint32_t OptionInfoSpritesMM[MaxTabCount][5]; + uint32_t OptionInfoSpritesPS4[MaxTabCount][5]; + + uint32_t SoundPrioSubhelpMM[3]; + uint32_t SoundPrioSubhelpPS4[3]; - static constexpr uint32_t TabInfoSprites[2][MaxTabCount] = { - { 3180940432, 4017317092 }, // FT UI - { 2099321196, 2806350346 } // MM+ UI - }; + uint32_t PS4WinTitleSpriteID; - static constexpr uint32_t OptionInfoSpritesMM[MaxTabCount][5] = { - { 0, 0, 0, 0, 0 }, - { 1445577118, 2528592817, 2367656052, 3568576596, 0 } - }; - static constexpr uint32_t OptionInfoSpritesPS4[MaxTabCount][5] = { - { 0, 0, 0, 0, 0 }, - { 2211731674, 2257174132, 2940751399, 160436885, 0 } - }; + void LoadAllSpriteSet() + { + std::string suffix = GetLanguageSuffix(); + for (auto& c : suffix) + c = std::toupper(c); + + const int OptionInfoIndices[] = { 6, 7, 8, 9 }; + + if (game::IsFutureToneMode()) + { + + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_NC_CUSTOM_FT_OPTIONS%02d%s", + nullptr, + 2, + TabInfoSpritesFT, + suffix.c_str() + ); - static constexpr uint32_t SoundPrioSubhelpMM[3] = { 2163775515, 1748614505, 2008681813 }; - static constexpr uint32_t SoundPrioSubhelpPS4[3] = { 2775564751, 2489232069, 3322578733 }; - static constexpr uint32_t PS4WinTitleSpriteID = 1861400143; + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_HELP_SUBTXT_NC_FT_%02d%s", + nullptr, + 3, + SoundPrioSubhelpPS4, + suffix.c_str() + ); + + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_HELP_TXT_NC_FT_%02d%s", + OptionInfoIndices, + 4, + OptionInfoSpritesPS4[1], + suffix.c_str() + ); + + LoadSpriteSet( + "SPR_NSWGAM_NCWIN_NC_CUSTOM_FT_TITLE%s", + suffix.c_str(), + &PS4WinTitleSpriteID + ); + } + else + { + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_NC_CUSTOM_OPTIONS%02d%s", + nullptr, + 2, + TabInfoSpritesMM, + suffix.c_str() + ); + + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_HELP_SUBTXT_NC_%02d%s", + nullptr, + 3, + SoundPrioSubhelpMM, + suffix.c_str() + ); + + LoadSpriteSetArray( + "SPR_NSWGAM_NCWIN_HELP_TXT_NC_%02d%s", + OptionInfoIndices, + 4, + OptionInfoSpritesMM[1], + suffix.c_str() + ); + } + } void CreateWindowBase() { @@ -197,6 +267,7 @@ class NCConfigWindow : public AetControl SetScene(SceneID); CreateWindowBase(); ChangeTab(0); + LoadAllSpriteSet(); } bool ShouldExit() const { return exit; } @@ -242,7 +313,7 @@ class NCConfigWindow : public AetControl { DrawSpriteAt("p_num_n_c", NumberSprites[0][selected_tab + 1]); DrawSpriteAt("p_num_d_c", NumberSprites[0][MaxTabCount]); - DrawSpriteAt("p_win_img_c", TabInfoSprites[0][selected_tab]); + DrawSpriteAt("p_win_img_c", TabInfoSpritesFT[selected_tab]); DrawSpriteAt("p_win_tit_lt", PS4WinTitleSpriteID); help_loc.DrawSpriteAt("p_help_loc_c", OptionInfoSpritesPS4[selected_tab][selected_option]); @@ -253,8 +324,8 @@ class NCConfigWindow : public AetControl { DrawSpriteAt("p_nc_page_num_10_c", NumberSprites[1][selected_tab + 1]); DrawSpriteAt("p_nc_page_num_01_c", NumberSprites[1][MaxTabCount]); - DrawSpriteAt("p_nc_img_02_c", TabInfoSprites[1][prev_selected_tab]); - DrawSpriteAt("p_nc_img_01_c", TabInfoSprites[1][selected_tab]); + DrawSpriteAt("p_nc_img_02_c", TabInfoSpritesMM[prev_selected_tab]); + DrawSpriteAt("p_nc_img_01_c", TabInfoSpritesMM[selected_tab]); help_loc.DrawSpriteAt("p_help_loc_c", OptionInfoSpritesMM[selected_tab][selected_option]); if (selected_tab == 1 && selected_option == 3) @@ -316,7 +387,7 @@ class NCConfigWindow : public AetControl { opt = std::make_unique( SceneID, - util::Format("ps4_options_base_nc_%02d_ft", id), + util::Format("ps4_options_base_nc_%02d_ft%s", id, GetLanguageSuffix().c_str()), WindowPrio, 14 ); @@ -327,7 +398,7 @@ class NCConfigWindow : public AetControl { opt = std::make_unique( SceneID, - util::Format("nsw_option_submenu_nc_%02d__f", id), + util::Format("nsw_option_submenu_nc_%02d__f%s", id, GetLanguageSuffix().c_str()), WindowPrio, 14 ); @@ -537,6 +608,25 @@ HOOK(bool, __fastcall, CustomizeSelTaskInit, 0x140687D10, uint64_t a1) return originalCustomizeSelTaskInit(a1); } + +void AddNCInputOptions() +{ + aet::Stop(&NCKeyInputFooter); + + std::string layerName = + (game::IsFutureToneMode() ? "ps4_" : "nsw_") + + std::string("key_nc") + + GetPlatformSuffix(GetInputType()) + + GetLanguageSuffix(); + + AetArgs args; + int prio = game::IsFutureToneMode() ? WindowPrio+2 : WindowPrio; + aet::CreateAetArgs(&args, SceneID, layerName.c_str(), prio); + args.res_mode = 14; + args.flags = 0x20000; + NCKeyInputFooter = aet::Play(&args, NCKeyInputFooter); +} + HOOK(bool, __fastcall, CustomizeSelTaskCtrl, 0x140687D70, uint64_t a1) { if (!cs_state.assets_loaded) @@ -547,12 +637,19 @@ HOOK(bool, __fastcall, CustomizeSelTaskCtrl, 0x140687D70, uint64_t a1) && !spr::CheckSprSetLoading(14020060); } + InputType current = GetInputType(); + if (current != previousInputType) + { + previousInputType = current; + AddNCInputOptions(); + } return originalCustomizeSelTaskCtrl(a1); } HOOK(bool, __fastcall, CustomizeSelTaskDest, 0x140687D80, uint64_t a1) { customize_sel::window.reset(); + aet::Stop(&NCKeyInputFooter); sound::UnloadFarc("rom/sound/se_nc.farc"); sound::UnloadFarc("rom/sound/se_nc_option.farc"); aet::UnloadAetSet(14010060); @@ -574,6 +671,20 @@ HOOK(void, __fastcall, CSTopMenuMainCtrl, 0x14069B610, uint64_t a1) originalCSTopMenuMainCtrl(a1); } +HOOK(void, __fastcall, PlayCustomizeSelFooter, 0x15F9811D0, void* a1, PlayCustomizeSelFooterArgs* args) +{ + if (args->footerName == "fotter01" && args->screen == 5) AddNCInputOptions(); + else aet::Stop(&NCKeyInputFooter); + originalPlayCustomizeSelFooter(a1, args); +} + +HOOK(void, __fastcall, StopCustomizeSelFooter, 0x140684A00, void* a1) +{ + aet::Stop(&NCKeyInputFooter); + originalStopCustomizeSelFooter(a1); +} + + void InstallCustomizeSelHooks() { INSTALL_HOOK(CustomizeSelTaskInit); @@ -581,4 +692,6 @@ void InstallCustomizeSelHooks() INSTALL_HOOK(CustomizeSelTaskDest); INSTALL_HOOK(CustomizeSelTaskDisp); INSTALL_HOOK(CSTopMenuMainCtrl); + INSTALL_HOOK(PlayCustomizeSelFooter); + INSTALL_HOOK(StopCustomizeSelFooter); } \ No newline at end of file