From 74efbf780a7d990a4669134b10343c09d896fffe Mon Sep 17 00:00:00 2001 From: GingerNinja Date: Sat, 17 Oct 2026 18:58:50 +0200 Subject: [PATCH] feat: prefer tech-applied ship in pinned ship level ties When several same-hull ships tied on level matched one pinned_ships entry, the winner was whichever ship the active sort listed first. Query ForbiddenTechManager (the same path the ship tech indicators use) for the ship's forbidden/chaos tech slots and let the ship with tech applied win the tie. The check is memoized per sort and only runs on actual level ties; bindings are lazily resolved and validated once, degrading to the previous behaviour with a single warning if unavailable. Adds HullId and Ship accessors to the prime FleetPlayerData header. --- mods/src/patches/parts/pinned_ship_sort.cc | 156 +++++++++++++++++++-- mods/src/prime/FleetPlayerData.h | 15 ++ 2 files changed, 162 insertions(+), 9 deletions(-) diff --git a/mods/src/patches/parts/pinned_ship_sort.cc b/mods/src/patches/parts/pinned_ship_sort.cc index b1b14e51e..9f3ff900c 100644 --- a/mods/src/patches/parts/pinned_ship_sort.cc +++ b/mods/src/patches/parts/pinned_ship_sort.cc @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -21,10 +22,18 @@ // everyone else. Only the concrete class is hooked, not ISortedFleetsContext, // so HailingFrequenciesInventoryContext is unaffected. Matching is shared // with the instant-warp filters (ship_name_match.h). If multiple ships share -// a pinned name, only the highest-level unclaimed one is pinned. +// a pinned name, only the highest-level unclaimed one is pinned; among +// same-hull ships tied on level, the one with forbidden/chaos tech applied +// wins. namespace { +// Forbidden tech / chaos tech slot spec ids, shared with the ship tech +// indicators patch (ship_tech_indicators.cc). A ship counts as "tech +// applied" when either slot holds an item. +constexpr int64_t kForbiddenTechSlotSpecId = 3502081615; +constexpr int64_t kChaosTechSlotSpecId = 953301906; + struct PinnedShipSortState { IL2CppClassHelper* shipManagementViewContext = nullptr; IL2CppFieldHelper* sortedIdleShipsField = nullptr; @@ -58,22 +67,147 @@ bool InitializeState() return true; } +// Bindings for asking ForbiddenTechManager whether a ship has forbidden or +// chaos tech equipped. Resolved once on first use; if anything is missing the +// tech tie-break quietly degrades to "no tech" (previous behaviour). +struct ShipTechQuery { + const MethodInfo* manager_instance = nullptr; + const MethodInfo* active_slots = nullptr; + const MethodInfo* slot_spec = nullptr; + const MethodInfo* slot_item = nullptr; + const MethodInfo* ship_id = nullptr; + bool valid = false; + bool resolved = false; +}; + +ShipTechQuery& TechQuery() +{ + static ShipTechQuery s; + return s; +} + +void ResolveShipTechQuery() +{ + auto& s = TechQuery(); + if (s.resolved) return; + s.resolved = true; + + auto manager = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.ForbiddenTechs", "ForbiddenTechManager"); + auto entity_slot = il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Models", "EntitySlot"); + auto base_ship = il2cpp_get_class_helper("Digit.Client.PrimeLib.Runtime", "Digit.PrimeServer.Models", "BaseShip"); + if (!manager.get_cls() || !entity_slot.get_cls() || !base_ship.get_cls()) { + spdlog::warn("[PinnedShipSort] forbidden tech bindings unavailable; tech tie-break disabled"); + return; + } + + s.manager_instance = manager.GetParent("MonoSingleton`1").GetMethodInfo("get_Instance", 0); + s.active_slots = manager.GetMethodInfo("TryGetActiveForbiddenTechSlot", 2); + s.slot_spec = method_contract::Resolve(entity_slot.get_cls(), "get_SlotSpecId", false, "System.Int64", {}); + s.slot_item = entity_slot.GetMethodInfo("get_SlotItemId", 0); + s.ship_id = method_contract::Resolve(base_ship.get_cls(), "get_Id", false, "System.Int64", {}); + + const bool manager_instance_valid = s.manager_instance && s.manager_instance->methodPointer + && (s.manager_instance->flags & METHOD_ATTRIBUTE_STATIC) + && s.manager_instance->parameters_count == 0 + && method_contract::Type(s.manager_instance->return_type, + "Digit.Prime.ForbiddenTechs.ForbiddenTechManager"); + const bool active_slots_valid = s.active_slots && s.active_slots->methodPointer + && !(s.active_slots->flags & METHOD_ATTRIBUTE_STATIC) + && method_contract::Type(s.active_slots->return_type, "System.Boolean") + && s.active_slots->parameters_count == 2 + && method_contract::Type(s.active_slots->parameters[0], "System.Int64") + && s.active_slots->parameters[1]->byref; + const bool slot_spec_valid = s.slot_spec != nullptr; + const bool slot_item_valid = s.slot_item && s.slot_item->methodPointer + && !(s.slot_item->flags & METHOD_ATTRIBUTE_STATIC) && s.slot_item->parameters_count == 0 + && s.slot_item->return_type->type == IL2CPP_TYPE_GENERICINST; + const bool ship_id_valid = s.ship_id != nullptr; + + s.valid = manager_instance_valid && active_slots_valid && slot_spec_valid && slot_item_valid && ship_id_valid; + if (!s.valid) { + spdlog::warn("[PinnedShipSort] forbidden tech bindings failed validation; tech tie-break disabled"); + } +} + +// True when the ship has forbidden tech or chaos tech equipped in either of +// its tech slots. Best-effort: any failure counts as "no tech". +bool HasAppliedTech(FleetPlayerData* ship) +{ + ResolveShipTechQuery(); + auto& s = TechQuery(); + if (!ship || !s.valid) return false; + + auto* ship_model = ship->__get_Ship(); + if (!ship_model) return false; + + Il2CppException* exc = nullptr; + auto* boxed_id = il2cpp_runtime_invoke(s.ship_id, ship_model, nullptr, &exc); + if (exc || !boxed_id) return false; + int64_t ship_id = *static_cast(il2cpp_object_unbox(boxed_id)); + + exc = nullptr; + auto* manager = il2cpp_runtime_invoke(s.manager_instance, nullptr, nullptr, &exc); + if (exc || !manager) return false; + + Il2CppObject* slots = nullptr; + void* slot_args[2] = {&ship_id, &slots}; + exc = nullptr; + auto* found = il2cpp_runtime_invoke(s.active_slots, manager, slot_args, &exc); + if (exc || !found || !*static_cast(il2cpp_object_unbox(found)) || !slots) return false; + + auto* slots_class = il2cpp_object_get_class(slots); + if (!slots_class) return false; + auto* get_count = il2cpp_class_get_method_from_name(slots_class, "get_Count", 0); + auto* get_item = il2cpp_class_get_method_from_name(slots_class, "get_Item", 1); + if (!get_count || !get_item) return false; + + exc = nullptr; + auto* count_obj = il2cpp_runtime_invoke(get_count, slots, nullptr, &exc); + if (exc || !count_obj) return false; + const auto count = *static_cast(il2cpp_object_unbox(count_obj)); + + for (int32_t i = 0; i < count; ++i) { + exc = nullptr; + void* idx[1] = {&i}; + auto* slot = il2cpp_runtime_invoke(get_item, slots, idx, &exc); + if (exc || !slot) continue; + + exc = nullptr; + auto* spec_obj = il2cpp_runtime_invoke(s.slot_spec, slot, nullptr, &exc); + if (exc || !spec_obj) continue; + const auto spec_id = *static_cast(il2cpp_object_unbox(spec_obj)); + if (spec_id != kForbiddenTechSlotSpecId && spec_id != kChaosTechSlotSpecId) continue; + + exc = nullptr; + auto* item = il2cpp_runtime_invoke(s.slot_item, slot, nullptr, &exc); + if (!exc && item) return true; // non-null Nullable: a tech is equipped + } + return false; +} + struct ShipEntry { - void* item; + void* item; std::vector match_words; // words from the game's own display name - int64_t level; + int64_t level; + int64_t hull_id; + int8_t tech_applied; // -1 unknown, 0 no, 1 yes (resolved lazily) + + // Memoized so the manager is only queried when a level tie actually happens. + bool TechApplied() + { + if (tech_applied < 0) { + tech_applied = HasAppliedTech(reinterpret_cast(item)) ? 1 : 0; + } + return tech_applied == 1; + } }; ShipEntry BuildShipEntry(void* item) { - ShipEntry entry{item, {}, 0}; - auto* ship = reinterpret_cast(item); - if (!ship) return entry; + if (!ship) return ShipEntry{item, {}, 0, 0, -1}; - entry.level = ship->Level; - entry.match_words = ShipNameMatch::DisplayWords(ship); - return entry; + return ShipEntry{item, ShipNameMatch::DisplayWords(ship), ship->Level, ship->HullId, -1}; } // Stable-partitions `list` (a List) so that ships matching @@ -144,6 +278,10 @@ void ReorderPinnedShips(void* list) if (ships[i].level > best_level) { best_level = ships[i].level; best_index = i; + } else if (best_index >= 0 && ships[i].level == best_level && ships[i].hull_id == ships[best_index].hull_id + && ships[i].TechApplied() && !ships[best_index].TechApplied()) { + // Same-level, same-hull tie: prefer the ship with forbidden/chaos tech applied. + best_index = i; } } diff --git a/mods/src/prime/FleetPlayerData.h b/mods/src/prime/FleetPlayerData.h index c969aa9ff..d290130ed 100644 --- a/mods/src/prime/FleetPlayerData.h +++ b/mods/src/prime/FleetPlayerData.h @@ -9,6 +9,8 @@ #include +struct Ship; + enum class FleetState { Unknown = 0, IdleInSpace = 1, @@ -47,6 +49,8 @@ struct FleetPlayerData { __declspec(property(get = __get_Address)) void* Address; __declspec(property(get = __get_Level)) int64_t Level; __declspec(property(get = __get_HasShip)) bool HasShip; + __declspec(property(get = __get_HullId)) int64_t HullId; + __declspec(property(get = __get_Ship)) ::Ship* Ship; private: static IL2CppClassHelper& get_class_helper() @@ -99,6 +103,17 @@ struct FleetPlayerData { auto* value = field.Get(this); return value ? *value : false; } + int64_t __get_HullId() + { + static auto property = get_class_helper().GetProperty("HullId"); + auto* value = property.Get(this); + return value ? *value : 0; + } + ::Ship* __get_Ship() + { + static auto property = get_class_helper().GetProperty("Ship"); + return property.GetRaw(this); + } int64_t GetLocaId() {