diff --git a/mods/src/patches/parts/chat_titles.cc b/mods/src/patches/parts/chat_titles.cc index b472d5338..11ad63913 100644 --- a/mods/src/patches/parts/chat_titles.cc +++ b/mods/src/patches/parts/chat_titles.cc @@ -18,42 +18,122 @@ Il2CppObject* InvokeSelf(Il2CppObject* object, const char* method_name) { auto* method = il2cpp_class_get_method_from_name(object ? il2cpp_object_get_class(object) : nullptr, method_name, 0); if (!method) { + static int missing_reports = 0; + if (missing_reports++ < 8) { + spdlog::warn("[ChatTitleDisplay] Cannot resolve {} on {}", method_name, + object ? il2cpp_class_get_name(il2cpp_object_get_class(object)) : "null"); + } return nullptr; } Il2CppException* exception = nullptr; - return (Il2CppObject*)il2cpp_runtime_invoke(method, object, nullptr, &exception); + auto* result = (Il2CppObject*)il2cpp_runtime_invoke(method, object, nullptr, &exception); + if (exception) { + static int exception_reports = 0; + if (exception_reports++ < 8) { + spdlog::warn("[ChatTitleDisplay] Managed invocation failed: {}", method_name); + } + return nullptr; + } + return result; } -bool IsChatRow(void* profile_widget) +Il2CppObject* FindChatRowTransform(void* profile_widget) { auto* mb = (Il2CppObject*)profile_widget; if (!mb) { - return false; + return nullptr; } auto* transform = InvokeSelf(mb, "get_transform"); if (!transform) { - return false; + return nullptr; } for (int hops = 0; hops < 32 && transform; ++hops) { auto* gameObject = InvokeSelf(transform, "get_gameObject"); if (!gameObject) { - return false; + return nullptr; } if (reinterpret_cast(gameObject)->GetComponentFastPath2()) { - return true; + return transform; } transform = InvokeSelf(transform, "get_parent"); } - return false; + return nullptr; } -void HideTitleRow(void* title) +struct TitleLayoutElement { + static IL2CppClassHelper& get_class_helper() + { + static auto helper = il2cpp_get_class_helper("UnityEngine.UI", "UnityEngine.UI", "LayoutElement"); + return helper; + } +}; + +void HideTitleRow(void* title, Il2CppObject* row_transform) { auto* game_object = reinterpret_cast(InvokeSelf(static_cast(title), "get_gameObject")); if (!game_object) { return; } game_object->SetActive(false); + + // In the chat prefab, _container is the enclosing PlayerTitleContainer, whose + // LayoutElement reserves 24 units even when the title widget child is hidden. + // Resolve the serialized reference rather than assuming a particular parent. + static auto* container_field = + il2cpp_class_get_field_from_name(il2cpp_object_get_class(static_cast(title)), "_container"); + GameObject* container = nullptr; + if (container_field) { + il2cpp_field_get_value(static_cast(title), container_field, &container); + } + if (container) { + static int container_reports = 0; + if (container_reports++ < 8) { + spdlog::info("[ChatTitleDisplay] Hiding title '{}' and container '{}' (active={})", game_object->Name(), + container->Name(), container->activeInHierarchy); + } + // The native bind path may reactivate the container after a nested title + // callback. Excluding its LayoutElement keeps that empty row out of layout. + if (auto* element = container->GetComponentFastPath2()) { + static auto ignore_layout = TitleLayoutElement::get_class_helper().GetMethodInfo("set_ignoreLayout", 1); + if (ignore_layout) { + bool ignore = true; + void* args[] = {&ignore}; + Il2CppException* exception = nullptr; + il2cpp_runtime_invoke(ignore_layout, element, args, &exception); + if (exception) { + spdlog::warn("[ChatTitleDisplay] Cannot exclude title container from layout"); + } + } + } + container->SetActive(false); + } else { + static bool reported = false; + if (!reported) { + reported = true; + spdlog::warn("[ChatTitleDisplay] Title '{}' has no container (field found={})", game_object->Name(), + container_field != nullptr); + } + } + + // Rebuild the complete message row before the virtualized list measures it; + // marking only the inner text layout leaves the enclosing row size cached. + static auto layout = il2cpp_get_class_helper("UnityEngine.UI", "UnityEngine.UI", "LayoutRebuilder"); + static auto rebuild = layout.GetMethodInfo("ForceRebuildLayoutImmediate", 1); + static auto mark = layout.GetMethodInfo("MarkLayoutForRebuild", 1); + for (auto* method : {rebuild, mark}) { + if (method) { + void* args[] = {row_transform}; + Il2CppException* exception = nullptr; + il2cpp_runtime_invoke(method, nullptr, args, &exception); + if (exception) { + static bool reported = false; + if (!reported) { + reported = true; + spdlog::warn("[ChatTitleDisplay] Chat row layout rebuild failed"); + } + } + } + } } void ApplyTitleDisplay(void* profile_widget) @@ -65,15 +145,26 @@ void ApplyTitleDisplay(void* profile_widget) const auto show = Config::Get().show_player_title; auto* title = widget->_playerTitleWidget; + static bool reported_callback = false; + if (!reported_callback) { + reported_callback = true; + spdlog::info("[ChatTitleDisplay] Profile callback reached (show={}, title present={})", show, title != nullptr); + } if (show || title == nullptr) { return; } - if (!IsChatRow(widget)) { + auto* row_transform = FindChatRowTransform(widget); + if (!row_transform) { + static bool reported = false; + if (!reported) { + reported = true; + spdlog::info("[ChatTitleDisplay] Profile callback excluded: no ChatMessageWidget ancestor"); + } return; } - HideTitleRow(title); + HideTitleRow(title, row_transform); } } // namespace @@ -85,7 +176,7 @@ void UserProfileWidget_UpdatePlayerTitle_Hook(auto original, void* _this) } catch (...) { } } -void PlayerTitleWidget_BindPath_Hook(auto original, void* _this) +void PlayerTitleWidget_SetIdentifiers_Hook(auto original, void* _this) { original(_this); try { @@ -146,76 +237,46 @@ void InstallChatTitleDisplayHooks() ErrorMsg::MissingMethod("UserProfileWidget", "UpdatePlayerTitle"); } - void* set_widget_data = nullptr; - void* on_did_bind_context = nullptr; + void* set_identifiers = nullptr; if (auto title_widget = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.PlayerAvatars", "PlayerTitleWidget"); !title_widget.isValidHelper()) { ErrorMsg::MissingHelper("Digit.Prime.PlayerAvatars", "PlayerTitleWidget"); - } else { - set_widget_data = title_widget.GetMethod("SetWidgetData", 0); - on_did_bind_context = title_widget.GetMethod("OnDidBindContext", 0); - if (!set_widget_data || !on_did_bind_context) { - ErrorMsg::MissingMethod("PlayerTitleWidget", set_widget_data ? "OnDidBindContext" : "SetWidgetData"); - set_widget_data = nullptr; - on_did_bind_context = nullptr; - } - } - -#if _WIN32 - // These small IL2CPP bind wrappers crash independently on the Windows - // client when detoured. Keep the verified-safe UpdatePlayerTitle hook and - // preserve the native bind paths. - if (set_widget_data || on_did_bind_context) { - spdlog::warn("[ChatTitleDisplay] skipping unsafe PlayerTitleWidget bind wrappers on Windows"); - set_widget_data = nullptr; - on_did_bind_context = nullptr; + } else if ((set_identifiers = title_widget.GetMethod("SetIdentifiers", 0)) == nullptr) { + ErrorMsg::MissingMethod("PlayerTitleWidget", "SetIdentifiers"); } -#endif - if (!update_player_title && !set_widget_data && !on_did_bind_context) { + if (!update_player_title && !set_identifiers) { return; } if (update_player_title) { update_player_title = ValidateDetourTarget(update_player_title, "UpdatePlayerTitle"); } - if (set_widget_data) { - set_widget_data = ValidateDetourTarget(set_widget_data, "SetWidgetData"); - } - if (on_did_bind_context) { - on_did_bind_context = ValidateDetourTarget(on_did_bind_context, "OnDidBindContext"); + if (set_identifiers) { + set_identifiers = ValidateDetourTarget(set_identifiers, "SetIdentifiers"); } - if (!update_player_title && !set_widget_data && !on_did_bind_context) { + if (!update_player_title && !set_identifiers) { spdlog::warn("Skipping ChatTitleDisplayHooks: no detour target fits; no hooks installed."); return; } - // The PlayerTitleWidget callbacks can resolve through thunks to one shared - // native implementation. A second detour on the same address would rebuild - // the first detour's jump into a broken trampoline, so keep one hook per - // target; the shared body funnels both method entries into the same hook. - if (set_widget_data && set_widget_data == update_player_title) { - spdlog::info("[ChatTitleDisplay] SetWidgetData shares its native implementation with UpdatePlayerTitle; " - "keeping one detour"); - set_widget_data = nullptr; - } - if (on_did_bind_context - && (on_did_bind_context == update_player_title || on_did_bind_context == set_widget_data)) { - spdlog::info("[ChatTitleDisplay] OnDidBindContext shares its native implementation with another target; " - "keeping one detour"); - on_did_bind_context = nullptr; + // Both title-bind callbacks tail-call SetIdentifiers. On Windows client 272 + // their shared wrapper is only 16 bytes, less than SPUD's 24-byte overwrite, + // so detouring it also corrupts the adjacent title-clear callback. Hook the + // substantive implementation once instead. + if (set_identifiers && set_identifiers == update_player_title) { + spdlog::warn("[ChatTitleDisplay] SetIdentifiers shares its target with UpdatePlayerTitle; keeping one detour"); + set_identifiers = nullptr; } if (update_player_title) { SPUD_STATIC_DETOUR(update_player_title, UserProfileWidget_UpdatePlayerTitle_Hook); } - if (set_widget_data) { - SPUD_STATIC_DETOUR(set_widget_data, PlayerTitleWidget_BindPath_Hook); - } - if (on_did_bind_context) { - SPUD_STATIC_DETOUR(on_did_bind_context, PlayerTitleWidget_BindPath_Hook); + if (set_identifiers) { + SPUD_STATIC_DETOUR(set_identifiers, PlayerTitleWidget_SetIdentifiers_Hook); } - spdlog::info("Chat title display: chat player title rows {}", Config::Get().show_player_title ? "shown" : "hidden"); + spdlog::info("Chat title display: chat player title rows {} (immediate row layout rebuild enabled)", + Config::Get().show_player_title ? "shown" : "hidden"); }