From c6da762988639841fc9ab0529ed1357281b05036 Mon Sep 17 00:00:00 2001 From: netniV Date: Sat, 24 Oct 2026 11:04:44 +0100 Subject: [PATCH] Add name to forbidden tech selections --- example_community_patch_settings_en.toml | 4 + mods/src/config.cc | 4 + mods/src/config.h | 2 + mods/src/defaultconfig.h | 2 + .../patches/parts/forbidden_tech_ship_name.cc | 346 ++++++++++++++++++ mods/src/patches/patches.cc | 2 + mods/src/ship_name_match.h | 10 +- mods/src/version.h | 2 +- 8 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 mods/src/patches/parts/forbidden_tech_ship_name.cc diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index be825d061..b45403a0c 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -293,6 +293,7 @@ chattitledisplayhooks = true dailyfactionbulkclaimhooks = true doubleclickassignshiphooks = true forbiddentechconfirmhooks = true +forbiddentechshipnamehooks = true freeresizehooks = true galacticanomalytimerhooks = true game_version = true @@ -733,6 +734,9 @@ only_show_first_popup = false # Show the newest Haven help-history entries first. reverse_haven_history = true +# Show the equipped ship below the Forbidden Tech header, including ships without a dock. +forbidden_tech_ship_name = true + # Subgroup: Auto Confirm # ---------------------- diff --git a/mods/src/config.cc b/mods/src/config.cc index 8102a74d8..be1a80525 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1013,6 +1013,8 @@ void Config::Load() get_config_or_default(config, parsed, "patches", "doubleclickassignshiphooks", DCP::doubleclickassignshiphooks, write_config); this->installForbiddenTechConfirmationHooks = get_config_or_default(config, parsed, "patches", "forbiddentechconfirmhooks", DCP::forbiddentechconfirmhooks, write_config); + this->installForbiddenTechShipNameHooks = get_config_or_default( + config, parsed, "patches", "forbiddentechshipnamehooks", DCP::forbiddentechshipnamehooks, write_config); this->installRevealConfirmationHooks = get_config_or_default(config, parsed, "patches", "revealconfirmhooks", DCP::revealconfirmhooks, write_config); this->installQueueAddressGuardHooks = get_config_or_default( @@ -1215,6 +1217,8 @@ void Config::Load() this->auto_confirm_ft_upgrade = get_config_or_default(config, parsed, "ui", "auto_confirm_ft_upgrade", DCU::auto_confirm_ft_upgrade, write_config); + this->forbidden_tech_ship_name = get_config_or_default( + config, parsed, "ui", "forbidden_tech_ship_name", DCU::forbidden_tech_ship_name, write_config); this->auto_confirm_reveal_ability = get_config_or_default(config, parsed, "ui", "auto_confirm_reveal_ability", DCU::auto_confirm_reveal_ability, write_config); diff --git a/mods/src/config.h b/mods/src/config.h index 5d5f74b80..2817d1616 100644 --- a/mods/src/config.h +++ b/mods/src/config.h @@ -236,6 +236,7 @@ class Config final NotificationSound alert_armada_battle_lost = NotificationSound::None; bool auto_open_bulk_claim_flyout; bool auto_confirm_ft_upgrade; + bool forbidden_tech_ship_name; bool auto_confirm_reveal_ability; bool auto_confirm_alliance_contribution; @@ -307,6 +308,7 @@ class Config final bool installLoadingTipHooks; bool installDoubleClickAssignShipHooks; bool installForbiddenTechConfirmationHooks; + bool installForbiddenTechShipNameHooks; bool installRevealConfirmationHooks; bool installInstantWarpConfirmationHooks; bool installAudioEventHooks; diff --git a/mods/src/defaultconfig.h b/mods/src/defaultconfig.h index b3e128516..ac58ef8b0 100644 --- a/mods/src/defaultconfig.h +++ b/mods/src/defaultconfig.h @@ -105,6 +105,7 @@ namespace Patches constexpr bool loadingtiphooks = true; constexpr bool doubleclickassignshiphooks = true; constexpr bool forbiddentechconfirmhooks = true; + constexpr bool forbiddentechshipnamehooks = true; constexpr bool audioeventhooks = true; constexpr bool actionqueuerecoveryhooks = true; constexpr bool queueaddressguardhooks = true; @@ -268,6 +269,7 @@ namespace UI constexpr bool dock_syncmanagement = true; constexpr bool auto_confirm_discovery = true; constexpr bool auto_confirm_ft_upgrade = false; + constexpr bool forbidden_tech_ship_name = true; constexpr bool auto_confirm_reveal_ability = false; constexpr bool auto_confirm_alliance_contribution = false; constexpr bool auto_open_bulk_claim_flyout = false; diff --git a/mods/src/patches/parts/forbidden_tech_ship_name.cc b/mods/src/patches/parts/forbidden_tech_ship_name.cc new file mode 100644 index 000000000..44faf1242 --- /dev/null +++ b/mods/src/patches/parts/forbidden_tech_ship_name.cc @@ -0,0 +1,346 @@ +#include "config.h" +#include "ship_name_match.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace +{ +struct Point { + float x, y; +}; +constexpr float RowHeight = 30.0f; + +// All value/byref calls below use runtime invocation: no platform-specific struct-return ABI. +Il2CppObject* Call(Il2CppObject* target, const char* name, int count = 0, void** args = nullptr) +{ + auto* method = target ? IL2CppClassHelper(target->klass).GetMethodInfo(name, count) : nullptr; + Il2CppObject* result = nullptr; + if (!method || !Il2CppRuntime::TryInvoke(method, target, args, &result)) + throw std::runtime_error(std::string("Forbidden Tech method unavailable: ") + name); + return result; +} + +template T Value(Il2CppObject* boxed, const char* type) +{ + if (!boxed || std::strcmp(boxed->klass->name, type) != 0) + throw std::runtime_error("Forbidden Tech value contract changed"); + return *static_cast(il2cpp_object_unbox(boxed)); +} + +bool Alive(Il2CppObject* object) +{ + if (!object) + return false; + static auto helper = il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine", "Object"); + static auto* method = helper.GetMethodInfo("op_Implicit", 1); + void* args[] = {object}; + Il2CppObject* result = nullptr; + return Il2CppRuntime::TryInvoke(method, nullptr, args, &result) && Il2CppChecked::Boolean(result); +} + +Point GetPoint(Il2CppObject* rect, const char* property) +{ return Value(Call(rect, property), "Vector2"); } + +void SetPoint(Il2CppObject* rect, const char* property, Point value) +{ + void* args[] = {&value}; + Call(rect, property, 1, args); +} + +void SetActive(Il2CppObject* component, bool active) +{ + auto* object = Call(component, "get_gameObject"); + void* args[] = {&active}; + Call(object, "SetActive", 1, args); +} + +Il2CppObject* Manager() +{ + static auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.ForbiddenTechs", "ForbiddenTechManager"); + static auto parent = helper.GetParent("MonoSingleton`1"); + static auto* instance = parent.GetMethodInfo("get_Instance", 0); + Il2CppObject* result = nullptr; + if (!Il2CppRuntime::TryInvoke(instance, nullptr, nullptr, &result)) + throw std::runtime_error("Forbidden Tech manager unavailable"); + return result; +} + +std::string EquippedShipName(Il2CppObject* widget) +{ + auto* context = Call(widget, "get_Context"); + auto* tech = context ? Il2CppChecked::Invoke(context, "get_ForbiddenTech") : nullptr; + if (!tech) + return {}; + auto id = Value(Call(tech, "get_Id"), "Int64"); + auto* manager = Manager(); + if (!manager) + return {}; + + // The fleet-index API returns -1 for a ship in the hangar. Resolve the slot's + // OwnerId instead; it identifies the actual ship regardless of dock assignment. + Il2CppObject* slot = nullptr; + void* slot_args[] = {&id, &slot}; + if (!Il2CppChecked::Boolean(Call(manager, "TryGetEntitySlotFromForbiddenTech", 2, slot_args)) || !slot) + return {}; + auto* params = Il2CppChecked::Invoke(slot, "get_ForbiddenTechSlotParams"); + if (!params) + return {}; + auto owner = Value(Call(params, "get_OwnerId"), "Int64"); + + auto* cache_field = Il2CppChecked::Field(manager, "_fleetServiceCache", "CachedService`1"); + auto* cache = il2cpp_field_get_value_object(cache_field, manager); + if (!cache || !il2cpp_class_is_valuetype(cache->klass)) + throw std::runtime_error("Forbidden Tech service cache contract changed"); + auto* getter = IL2CppClassHelper(cache->klass).GetMethodInfo("get_Service", 0); + Il2CppObject* service = nullptr; + if (!Il2CppRuntime::TryInvoke(getter, il2cpp_object_unbox(cache), nullptr, &service) || !service) + return {}; + void* ship_args[] = {&owner}; + auto* ship = Call(service, "GetShip", 1, ship_args); + auto* hull = ship ? Il2CppChecked::Invoke(ship, "get_Hull") : nullptr; + if (!hull) + return {}; + // Hull IDs and localisation IDs differ on many ships. Mirror FleetPlayerData.GetLocaId. + auto* refs = Il2CppChecked::Invoke(hull, "get_IdRefs"); + const auto loca_id = Value(Call(refs ? refs : hull, refs ? "get_LocaId" : "get_Id"), "Int64"); + return ShipNameMatch::LocalizedShipName(loca_id); +} + +struct Row { + Il2CppGCHandle scroller = nullptr; + Il2CppGCHandle label = nullptr; + Point original_top{}; + bool visible = false; + + Il2CppObject* Scroller() const { return il2cpp_gchandle_get_target(scroller); } + Il2CppObject* Label() const { return label ? il2cpp_gchandle_get_target(label) : nullptr; } + + void Hide() + { + if (Alive(Label())) + SetActive(Label(), false); + if (visible && Alive(Scroller())) + SetPoint(Scroller(), "set_offsetMax", original_top); + visible = false; + } + + void Free() + { + if (scroller) + il2cpp_gchandle_free(scroller); + if (label) + il2cpp_gchandle_free(label); + } +}; +std::vector rows; + +void HideWidgetRow(Il2CppObject* widget) +{ + auto* scroller = Il2CppChecked::ReferenceField(widget, "_buffListScroller", "SmartScroller"); + if (!Alive(scroller)) + return; + auto* rect = Call(scroller, "get_transform"); + for (auto& row : rows) { + if (row.Scroller() == rect) + row.Hide(); + } +} + +Il2CppObject* CloneLabel(Il2CppObject* source, Il2CppObject* parent) +{ + static auto helper = il2cpp_get_class_helper("UnityEngine.CoreModule", "UnityEngine", "Object"); + // This client exposes the two-argument overload only as Instantiate. + // Use the non-generic three-argument overload so no generic inflation is required. + static auto* instantiate = helper.GetMethodInfoSpecial("Instantiate", [](int count, const Il2CppType** types) { + return count == 3 && types && types[0] && types[1] && types[2] + && !types[0]->byref && !types[1]->byref && !types[2]->byref + && il2cpp_class_from_type(types[0]) + && std::strcmp(il2cpp_class_from_type(types[0])->name, "Object") == 0 + && il2cpp_class_from_type(types[1]) + && std::strcmp(il2cpp_class_from_type(types[1])->name, "Transform") == 0 + && types[2]->type == IL2CPP_TYPE_BOOLEAN; + }); + if (!instantiate || instantiate->is_generic || !(instantiate->flags & METHOD_ATTRIBUTE_STATIC)) + throw std::runtime_error("Forbidden Tech label Instantiate(Object, Transform, Boolean) unavailable"); + bool world_position_stays = false; + void* args[] = {source, parent, &world_position_stays}; + Il2CppObject* result = nullptr; + if (!Il2CppRuntime::TryInvoke(instantiate, nullptr, args, &result) || !Alive(result)) + throw std::runtime_error("Forbidden Tech ship label could not be created"); + return result; +} + +void UpdateRow(Il2CppObject* widget) +{ + // Only the large selected-item panel: inventory tiles retain their native dock badges. + auto* type_field = Il2CppChecked::Field(widget, "_widgetType", "WidgetType"); + int32_t type = 0; + il2cpp_field_get_value(widget, type_field, &type); + if (type != 1) + return; + + auto* scroller = Il2CppChecked::ReferenceField(widget, "_buffListScroller", "SmartScroller"); + if (!Alive(scroller)) + return; + auto* rect = Call(scroller, "get_transform"); + if (!rect || std::strcmp(rect->klass->name, "RectTransform") != 0) + throw std::runtime_error("Forbidden Tech buff layout unavailable"); + + std::erase_if(rows, [](Row& row) { + if (Alive(row.Scroller())) + return false; + row.Free(); + return true; + }); + auto found = std::find_if(rows.begin(), rows.end(), [rect](const Row& row) { return row.Scroller() == rect; }); + if (found == rows.end()) { + Row row; + row.original_top = GetPoint(rect, "get_offsetMax"); + row.scroller = il2cpp_gchandle_new_weakref(rect, false); + if (!row.scroller) + return; + rows.push_back(row); + found = std::prev(rows.end()); + } + auto& row = *found; + // Clear before resolving a new assignment so errors/unassigned tech cannot retain an old ship name. + row.Hide(); + // Native refresh may have resized the panel since the previous selection. + row.original_top = GetPoint(rect, "get_offsetMax"); + if (!Config::Get().forbidden_tech_ship_name) + return; + const auto name = EquippedShipName(widget); + if (name.empty()) + return; + + try { + auto* label = row.Label(); + if (!Alive(label)) { + auto* source = Il2CppChecked::ReferenceField(widget, "_name", "TextLocalizer"); + if (!Alive(source)) + return; + label = CloneLabel(source, Call(rect, "get_parent")); + SetActive(label, false); + if (row.label) + il2cpp_gchandle_free(row.label); + row.label = il2cpp_gchandle_new_weakref(label, false); + if (!row.label) + throw std::runtime_error("Forbidden Tech label handle unavailable"); + } + auto* label_rect = Call(label, "get_transform"); + const auto min = GetPoint(rect, "get_offsetMin"); + const auto max = row.original_top; + const auto anchors = GetPoint(rect, "get_anchorMax"); + auto anchor_min = GetPoint(rect, "get_anchorMin"); + anchor_min.y = anchors.y; + SetPoint(label_rect, "set_anchorMin", anchor_min); + SetPoint(label_rect, "set_anchorMax", anchors); + SetPoint(label_rect, "set_pivot", {0.5f, 1.0f}); + SetPoint(label_rect, "set_offsetMin", {min.x, max.y - RowHeight}); + SetPoint(label_rect, "set_offsetMax", max); + + // Target resolves the graphic lazily, even when the source label was inactive at clone time. + auto* text = Il2CppChecked::Invoke(label, "get_Target"); + static auto tmp = il2cpp_get_class_helper("Unity.TextMeshPro", "TMPro", "TMP_Text"); + if (!text || !tmp.get_cls() || !il2cpp_class_is_assignable_from(tmp.get_cls(), text->klass)) + throw std::runtime_error("Forbidden Tech ship label text unavailable"); + int32_t alignment = 513; // TMP TextAlignmentOptions.Left (left, vertically centred). + bool raycast = false; + bool rich_text = false; // Ship names are plain text, never markup. + bool wrap = false; + bool auto_size = false; + float font_size = 20.0f; + struct { float r, g, b, a; } color{1.0f, 1.0f, 1.0f, 1.0f}; + int32_t overflow = 1; // TMP TextOverflowModes.Ellipsis; long names stay within this row. + void* align_args[] = {&alignment}; + void* raycast_args[] = {&raycast}; + void* rich_args[] = {&rich_text}; + void* wrap_args[] = {&wrap}; + void* overflow_args[] = {&overflow}; + void* auto_size_args[] = {&auto_size}; + void* font_args[] = {&font_size}; + void* color_args[] = {&color}; + Call(text, "set_alignment", 1, align_args); + Call(text, "set_raycastTarget", 1, raycast_args); + Call(text, "set_richText", 1, rich_args); + Call(text, "set_enableWordWrapping", 1, wrap_args); + Call(text, "set_overflowMode", 1, overflow_args); + Call(text, "set_enableAutoSizing", 1, auto_size_args); + Call(text, "set_fontSize", 1, font_args); + Call(text, "set_color", 1, color_args); + auto* value = il2cpp_string_new(("Equipped on: " + name).c_str()); + void* text_args[] = {value}; + Call(label, "OverrideLocalizedText", 1, text_args); + // Keep the panel's outer size, reserving one row from the scrollable buff area. + SetPoint(rect, "set_offsetMax", {max.x, max.y - RowHeight}); + row.visible = true; + SetActive(label, true); + static bool reported = false; + if (!reported) { + reported = true; + spdlog::info("[ForbiddenTechShipName] equipped-ship row created and populated"); + } + } catch (...) { + row.Hide(); + throw; + } +} + +void SetWidgetData_Hook(auto original, Il2CppObject* widget) +{ + try { + HideWidgetRow(widget); + } catch (const std::exception&) { + } + original(widget); + try { + UpdateRow(widget); + } catch (const std::exception& error) { + static bool warned = false; + if (!warned) { + warned = true; + spdlog::warn("[ForbiddenTechShipName] native display retained: {}", error.what()); + } + } +} + +void ClearWidgetData_Hook(auto original, Il2CppObject* widget) +{ + try { + HideWidgetRow(widget); + } catch (const std::exception&) { + } + original(widget); +} +} // namespace + +void InstallForbiddenTechShipNameHooks() +{ + auto helper = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.ForbiddenTechs", "ForbiddenTechInventoryItemWidget"); + auto* method = helper.GetMethodInfo("SetWidgetData", 0); + auto* clear = helper.GetMethodInfo("ClearWidgetData", 0); + // Both methods perform substantive native UI updates, rather than forwarding through tiny wrappers. + const auto valid = [&helper](const MethodInfo* info) { + return info && info->klass == helper.get_cls() && info->methodPointer && !info->is_generic + && !info->is_inflated && !(info->flags & METHOD_ATTRIBUTE_STATIC) && info->parameters_count == 0 + && info->return_type && info->return_type->type == IL2CPP_TYPE_VOID; + }; + if (!valid(method) || !valid(clear)) { + spdlog::warn("[ForbiddenTechShipName] widget refresh unavailable; native display retained"); + return; + } + // Install cleanup first; failure leaves the native panel entirely untouched. + if (!SPUD_STATIC_DETOUR(clear->methodPointer, ClearWidgetData_Hook)) { + spdlog::warn("[ForbiddenTechShipName] widget cleanup hook unavailable; native display retained"); + return; + } + const bool installed = SPUD_STATIC_DETOUR(method->methodPointer, SetWidgetData_Hook) != nullptr; + spdlog::info("[ForbiddenTechShipName] widget refresh hook {}", installed ? "installed" : "unavailable"); +} diff --git a/mods/src/patches/patches.cc b/mods/src/patches/patches.cc index 7a3083607..546c2c440 100644 --- a/mods/src/patches/patches.cc +++ b/mods/src/patches/patches.cc @@ -54,6 +54,7 @@ void InstallPinnedShipClickHooks(); void InstallDoubleClickAssignShipHooks(); void InstallInstantWarpConfirmationHooks(); void InstallForbiddenTechConfirmationHooks(); +void InstallForbiddenTechShipNameHooks(); void InstallRevealConfirmationHooks(); void InstallAudioEventHooks(); void InstallActionQueueRecovery(); @@ -178,6 +179,7 @@ __int64 il2cpp_init_hook(auto original, const char* domain_name) {"DoubleClickAssignShip", {InstallDoubleClickAssignShipHooks, &cfg.installDoubleClickAssignShipHooks}}, {"InstantWarpConfirm", {InstallInstantWarpConfirmationHooks, &cfg.installInstantWarpConfirmationHooks}}, {"MessageBoxConfirmations", {InstallForbiddenTechConfirmationHooks, &cfg.installForbiddenTechConfirmationHooks}}, + {"ForbiddenTechShipName", {InstallForbiddenTechShipNameHooks, &cfg.installForbiddenTechShipNameHooks}}, {"RevealConfirm", {InstallRevealConfirmationHooks, &cfg.installRevealConfirmationHooks}}, {"AudioEvents", {InstallAudioEventHooks, &cfg.installAudioEventHooks}}, {"ActionQueueRecovery", {InstallActionQueueRecovery, &cfg.installActionQueueRecoveryHooks}}, diff --git a/mods/src/ship_name_match.h b/mods/src/ship_name_match.h index 781079b6c..1c9d2868a 100644 --- a/mods/src/ship_name_match.h +++ b/mods/src/ship_name_match.h @@ -73,11 +73,8 @@ inline bool EndsWithWords(const std::vector& candidate, const std:: return true; } -inline std::string GameDisplayName(FleetPlayerData* ship) +inline std::string LocalizedShipName(int64_t loca_id) { - if (!ship || !ship->Hull) return {}; - - const auto loca_id = ship->GetLocaId(); if (loca_id <= 0) return {}; struct Resolved { @@ -126,6 +123,11 @@ inline std::string GameDisplayName(FleetPlayerData* ship) return {}; } +inline std::string GameDisplayName(FleetPlayerData* ship) +{ + return ship && ship->Hull ? LocalizedShipName(ship->GetLocaId()) : std::string{}; +} + inline std::vector DisplayWords(FleetPlayerData* ship) { if (!ship) return {}; diff --git a/mods/src/version.h b/mods/src/version.h index 1f6c6bff9..d50c61dab 100644 --- a/mods/src/version.h +++ b/mods/src/version.h @@ -4,7 +4,7 @@ #define VERSION_MAJOR 1 #define VERSION_MINOR 1 #define VERSION_REVISION 9 -#define VERSION_PATCH 4 +#define VERSION_PATCH 5 #define VERSION_COMMIT_HASH "" #define STRINGIFY_(s) #s