Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 147 additions & 9 deletions mods/src/patches/parts/pinned_ship_sort.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <il2cpp/il2cpp-functions.h>
#include <il2cpp/il2cpp_helper.h>
#include <il2cpp/method_contract.h>

#include <spdlog/spdlog.h>
#include <spud/detour.h>
Expand All @@ -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;
Expand Down Expand Up @@ -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<int64_t*>(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<bool*>(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<int32_t*>(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<int64_t*>(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<Int64>: a tech is equipped
}
return false;
}

struct ShipEntry {
void* item;
void* item;
std::vector<std::string> 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<FleetPlayerData*>(item)) ? 1 : 0;
}
return tech_applied == 1;
}
};

ShipEntry BuildShipEntry(void* item)
{
ShipEntry entry{item, {}, 0};

auto* ship = reinterpret_cast<FleetPlayerData*>(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<FleetPlayerData>) so that ships matching
Expand Down Expand Up @@ -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;
}
}

Expand Down
15 changes: 15 additions & 0 deletions mods/src/prime/FleetPlayerData.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

#include <cstdint>

struct Ship;

enum class FleetState {
Unknown = 0,
IdleInSpace = 1,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -99,6 +103,17 @@ struct FleetPlayerData {
auto* value = field.Get<bool>(this);
return value ? *value : false;
}
int64_t __get_HullId()
{
static auto property = get_class_helper().GetProperty("HullId");
auto* value = property.Get<int64_t>(this);
return value ? *value : 0;
}
::Ship* __get_Ship()
{
static auto property = get_class_helper().GetProperty("Ship");
return property.GetRaw<struct Ship>(this);
}

int64_t GetLocaId()
{
Expand Down
Loading