mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-20 09:26:27 +02:00
Skater slot, menu and throwdown fixes; MY MODS rework; lobby parties Saved skaters (Extension/Customization, Extension/Skater) - The selected slot, and slots before it, came up as the standard skater after a start or map change. The game asks for a slot's saved outfit once per level, and that happened before the cosmetics catalog could be read. The first selection now waits for the catalog (60 s at most), and later map changes check against the catalog already read. - The retry fromabdda77is removed: the record it wrote to is a temporary on the game's stack, so it could never work. Pause menu (Extension/UI/NativeMenu) - ReSkate's entries stopped working after enough map changes: each level change took new slots in a fixed table of 1024 for the same buttons. A button now keeps its slot, and a full table hands out the slot that has gone longest unused. Throwdowns (Extension/Throwdowns, Runtime/runtime_init.cpp) - Multiplayer crashed when someone placed a throwdown after a map change. The event types are freed with the level but were cached for the process. They are now cleared when a level is left, checked before use, and looked for again in each level. MY MODS (Launcher/gui_mods.cpp, gui.cpp) - Search, a filter and an order; a switch per mod; a menu per row. - Multi-select with tick boxes, Shift-click and Ctrl+A, and a bar to enable, disable, move, update or uninstall everything ticked. Lobby parties (Extension/Multiplayer/Session) - A lobby is no longer one party. Its host keeps parties the way a dedicated server does: invite, accept, join, leave, kick, promote, open and close, with "/p" party chat. The "Lobby party" switch is gone. Protocol version unchanged (38). Mod merge (Engine/Vfs), follow-ups toe715b9b- Files rewritten into the merged patch are tracked by position, so another mod's added assets no longer shift the lookup. - Two mods adding different assets under one name is now noted, with whose copy the maps get. - mod_merge_unshift_tests compiles with the launcher tests on.
493 lines
30 KiB
C++
493 lines
30 KiB
C++
#include "native_party_internal.h"
|
|
#include "Extension/Multiplayer/Steam/steam_social.h"
|
|
#include "Extension/Customization/local_customization_runtime.h"
|
|
#include "Extension/Customization/local_player_card_runtime.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
using namespace native_party_detail;
|
|
namespace {
|
|
// The authored Party panel has eight rows, and the party colour list eight colours.
|
|
constexpr unsigned authored_party_slots = 8;
|
|
// The type an entry of the registry holds when it holds the type named by hash.
|
|
Address registered_type(Address entry, std::uint32_t hash) {
|
|
const auto first = read<Address>(entry + 8), last = read<Address>(entry + 16);
|
|
if (!first || last <= first) return 0;
|
|
const auto type = read<Address>(first);
|
|
return type && read<std::uint32_t>(read<Address>(type)) == hash ? type : 0;
|
|
}
|
|
Address type_with_hash(Address manager, std::uint32_t hash) {
|
|
// Registered native model types, not a process-memory/string scan. Dynamic
|
|
// UI schemas are registered again when their asset partition is loaded.
|
|
const auto begin = read<Address>(manager + 0x600), end = read<Address>(manager + 0x608);
|
|
require(begin && end >= begin && (end - begin) % 72 == 0 && (end - begin) / 72 <= 65536,
|
|
"Native party model type registry differs.");
|
|
// The registry holds thousands of types. While it is the same array, the entry
|
|
// found last time is checked again instead (the same test the walk applies);
|
|
// a type not found is looked for again once the array changes, or after 1 s.
|
|
auto &cached = state().types[hash];
|
|
const bool same = cached.manager == manager && cached.begin == begin && cached.end == end;
|
|
if (same && cached.type && registered_type(cached.entry, hash) == cached.type) return cached.type;
|
|
const auto now = GetTickCount64();
|
|
if (same && !cached.type && now < cached.retry_at) return 0;
|
|
for (auto at = begin; at < end; at += 72) {
|
|
if (const auto type = registered_type(at, hash)) {
|
|
cached = {manager, begin, end, at, type};
|
|
return type;
|
|
}
|
|
}
|
|
cached = {manager, begin, end, 0, 0, now + 1000};
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
namespace native_party_detail {
|
|
Address registered_model_type(Address manager, std::uint32_t hash) { return type_with_hash(manager, hash); }
|
|
void require(bool ok, const char *text) { if (!ok) throw std::runtime_error(text); }
|
|
State &state() { static auto *s = new State; return *s; }
|
|
void status(std::string message) {
|
|
auto &s = state();
|
|
std::lock_guard lock(s.status_mutex);
|
|
if (message == s.status) return;
|
|
s.status = std::move(message);
|
|
logging::write(logging::Level::info, logging::Channel::ui, s.status);
|
|
}
|
|
Address model_type(Address manager, Handle handle) {
|
|
const auto base = state().base;
|
|
const auto record = reinterpret_cast<Address (*)(Address, Handle, std::uint8_t)>(base + party::model_record)(manager, handle, 0);
|
|
if (!record) return 0;
|
|
return reinterpret_cast<Address (*)(Address, Handle, Address, std::uint8_t)>(base + party::model_type)(manager, handle, record, 0);
|
|
}
|
|
Handle singleton(Address manager, std::uint32_t hash, std::uint16_t size) {
|
|
const auto type = type_with_hash(manager, hash);
|
|
if (!type) return 0;
|
|
require(read<std::uint16_t>(read<Address>(type) + 6) == size, "Native party singleton size differs.");
|
|
return reinterpret_cast<Handle (*)(Address, Address, std::uint8_t, std::uint8_t)>(state().base + party::model_singleton)(
|
|
manager, type, 0, 1);
|
|
}
|
|
Handle field(Address manager, Handle parent, unsigned index, std::uint32_t hash, std::uint16_t offset) {
|
|
const auto type = model_type(manager, parent);
|
|
require(type, "Native party model handle expired.");
|
|
const auto meta = read<Address>(type);
|
|
require(index < read<std::uint16_t>(meta + 0x2a), "Native party field count differs.");
|
|
const auto entry = read<Address>(meta + 0x60) + index * 24;
|
|
require(read<std::uint32_t>(entry) == hash && read<std::uint16_t>(entry + 8) == offset,
|
|
"Native party field schema differs.");
|
|
const auto result = game::native_data().models.field(manager, parent, index, UINT32_MAX, false);
|
|
require(result && model_type(manager, result) == read<Address>(entry + 16), "Native party child type differs.");
|
|
return result;
|
|
}
|
|
} // namespace native_party_detail
|
|
namespace {
|
|
template<std::size_t N, class T> void put(std::array<std::byte, N> &bytes, std::size_t offset, T value) {
|
|
require(offset + sizeof(value) <= N, "Native party record bounds differ.");
|
|
std::memcpy(bytes.data() + offset, &value, sizeof(value));
|
|
}
|
|
// A player's own party from the roster. `shown` false (the local party of a lobby shown without
|
|
// the party count preference) reports none, as before.
|
|
GroupStatus group_status(const PartyRoster &roster, unsigned capacity, const PartyPlayer &player, bool shown) {
|
|
GroupStatus g;
|
|
g.limit = static_cast<std::uint8_t>(std::clamp(capacity, 1U, 255U));
|
|
if (!player.party || !shown) return g;
|
|
std::uint8_t members{};
|
|
std::uint64_t leader{};
|
|
for (const auto &other : roster)
|
|
if (other.id && other.party == player.party) {
|
|
++members;
|
|
if (other.leader_of_party) leader = other.id;
|
|
}
|
|
if (members < 2) return g;
|
|
g.id = native_party_group_id(player.party);
|
|
g.members = members;
|
|
g.leader = player.leader_of_party;
|
|
g.leader_id = leader;
|
|
g.join = player.party_open ? 2 : 1;
|
|
return g;
|
|
}
|
|
Handle player_record(Address manager, const PartyPlayer &player, std::uint32_t player_id,
|
|
bool party = true, bool friend_record = false, bool online = true, std::int32_t colour = -1,
|
|
const GroupStatus &group = {}) {
|
|
auto &s = state(); auto &n = game::native_data().models;
|
|
const auto key = std::pair{player.id, player.epoch};
|
|
auto found = s.handles.find(key);
|
|
Handle handle = found == s.handles.end() ? 0 : found->second;
|
|
if (!handle || !n.value(manager, handle, 0, 0)) {
|
|
require(s.handles.size() < 8192, "Native social record limit reached; reload the map to refresh UI models.");
|
|
// Distinct from the native service namespace: these IDs never register
|
|
// an EA account or a ClientPlayer. Native UI lookups are adapted below.
|
|
handle = n.create(manager, s.base + engine::player_info_type, player.id,
|
|
game::native_name_hash("ReSkate.SteamParty") ^ static_cast<std::uint32_t>(player.epoch) ^
|
|
static_cast<std::uint32_t>(player.epoch >> 32), false, 2);
|
|
require(handle, "Cannot create native party player info.");
|
|
s.handles[key] = handle; s.owned.insert(handle); s.owned_ids.insert(player.id);
|
|
}
|
|
require(model_type(manager, handle) == s.base + engine::player_info_type, "Native party player info type differs.");
|
|
alignas(8) std::array<std::byte, 0x108> info{};
|
|
reinterpret_cast<void (*)(void *)>(s.base + engine::card_info_construct)(info.data());
|
|
put(info, 0x00, group.join); put(info, 0x04, group.invite); put(info, 0x08, group.id);
|
|
put(info, 0x10, group.members); put(info, 0x11, group.limit); put(info, 0x12, group.leader);
|
|
put(info, 0x18, group.leader_id); put(info, 0x20, group.unlocked);
|
|
// Social actions (Invite) need the player "unlocked" (+0x95); friends are IsFriend (+0x104).
|
|
put(info, 0x95, std::uint8_t{1});
|
|
put(info, 0x104, friend_record);
|
|
const NativeText text(player.name.empty() ? "Skater" : player.name);
|
|
put(info, 0x28, text.value); put(info, 0x38, text.value);
|
|
put(info, 0x40, player.id); put(info, 0x4c, player_id);
|
|
put(info, 0xa0, handle);
|
|
put(info, 0xf0, std::uint32_t{player.local ? 0U : party ? 1U : online ? 4U : 5U});
|
|
put(info, 0xcc, std::uint32_t{friend_record ? (party ? 3U : 2U) : 1U});
|
|
put(info, 0xd8, std::uint32_t{online ? 0U : 2U});
|
|
// Player network (EadpPlayerNetwork): every ReSkate player is on Steam; left at 0 the
|
|
// UI shows the EA logo beside the name.
|
|
put(info, 0xf4, std::uint32_t{4});
|
|
const auto card_part = [&](std::uint32_t hash) {
|
|
return !player.local && profile_runtime::reserved_cosmetic_hash(hash) ? 0U : hash;
|
|
};
|
|
put(info, 0xd0, card_part(player.card.background)); put(info, 0xc4, card_part(player.card.emblem));
|
|
put(info, 0xd4, card_part(player.card.title));
|
|
// The party colour index (map dots, waypoints, the party list and voice HUD pick their
|
|
// colour from PartyMemberColorsList by it) and the mic state the voice HUD shows.
|
|
put(info, 0xc0, party ? colour : std::int32_t{-1});
|
|
put(info, 0xdc, std::int32_t{player.mic});
|
|
put(info, 0x100, player.present); put(info, 0x101, party);
|
|
put(info, 0x105, player.local); put(info, 0x107, player.present);
|
|
n.publish(manager, handle, s.base + engine::player_info_type, info.data());
|
|
require(n.value(manager, handle, 0, 0), "Native party player info publication failed.");
|
|
return handle;
|
|
}
|
|
void social_list(Address ui, Address manager, const Published &published, bool friends = false) {
|
|
auto &n = game::native_data().models;
|
|
const auto handle = read<Handle>(ui + (friends ? 0xf8 : 0xf0));
|
|
require(handle && model_type(manager, handle) == state().base + party::player_list_type,
|
|
"Native social player list type differs.");
|
|
struct Ref { Address value{}; Handle context{}; };
|
|
game::NativeArrayStorage<Ref, 2304> next;
|
|
Address data{}; std::uint32_t count{};
|
|
const auto value = n.value(manager, handle, 0, 0);
|
|
require(value && game::native_array<Ref>(reinterpret_cast<const void *>(value), data, count, 2304),
|
|
"Native social player list exceeds bound.");
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
const auto item = read<Ref>(data + i * sizeof(Ref));
|
|
if (!state().owned.contains(item.context)) {
|
|
require(next.count < 248, "Native social list has too many non-Steam records.");
|
|
next.values[next.count++] = item;
|
|
}
|
|
}
|
|
if (friends) {
|
|
for (const auto &record : published.friends) if (record.handle) next.values[next.count++] = {0, record.handle};
|
|
} else for (const auto &record : published.records) {
|
|
if (record.handle && !record.player.local) {
|
|
// Native ModelRef is a reference-counted boxed value plus a model
|
|
// handle. The native UI roster uses a null boxed value here.
|
|
next.values[next.count++] = {0, record.handle};
|
|
}
|
|
}
|
|
const auto pointer = next.values.data();
|
|
n.publish(manager, handle, state().base + party::player_list_type, &pointer);
|
|
if (friends) {
|
|
const auto root = singleton(manager, 0xdd652425, 40);
|
|
if (root) {
|
|
game::NativeArrayStorage<Handle, 2304> handles;
|
|
for (unsigned i = 0; i < next.count; ++i) handles.values[handles.count++] = next.values[i].context;
|
|
const auto values = handles.values.data();
|
|
publish_value(manager, field(manager, root, 0, 0x7138dc31, 0), state().base + party::uint64_array_type, values);
|
|
publish_value(manager, field(manager, root, 1, 0xa19aaa9b, 8), state().base + engine::bool_type, true);
|
|
const auto online = static_cast<std::int32_t>(std::count_if(published.friends.begin(), published.friends.end(),
|
|
[&](const auto &record) {
|
|
const auto info = n.value(manager, record.handle, 0, 0);
|
|
return info && read<std::uint32_t>(info + 0xd8) == 0;
|
|
}));
|
|
publish_value(manager, field(manager, root, 4, 0xbe820091, 32), state().base + engine::int32_type, online);
|
|
}
|
|
}
|
|
}
|
|
void friends_view(Address manager, const Published &published) {
|
|
auto &s = state(); auto &n = game::native_data().models;
|
|
// The current Friends tab observes UIFriendsService's sorted list, not
|
|
// UIPlayerManager.Friends or the legacy FriendsData singleton alone.
|
|
const auto service = read<Address>(s.base + party::friends_service);
|
|
if (!service || read<Address>(service) != s.base + party::friends_service_vtable) return;
|
|
const auto handle = read<Handle>(service + 0x120);
|
|
require(handle && model_type(manager, handle) == s.base + party::sorted_friends_type,
|
|
"Native sorted friends model differs.");
|
|
Address rows{}; std::uint32_t count{};
|
|
const auto value = n.value(manager, handle, 0, 0);
|
|
require(value && game::native_array<Handle>(reinterpret_cast<const void *>(value), rows, count, 2304),
|
|
"Native sorted friends list exceeds bound.");
|
|
game::NativeArrayStorage<Handle, 2304> next;
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
const auto item = read<Handle>(rows + i * sizeof(Handle));
|
|
if (!s.owned.contains(item)) {
|
|
require(next.count < 248, "Native sorted friends list has too many non-Steam records.");
|
|
next.values[next.count++] = item;
|
|
}
|
|
}
|
|
// Authored FriendsFilter: 0 = all, 1 = EA-only, 2 = platform-only.
|
|
const auto filter = read<std::uint32_t>(service + 0x130);
|
|
if (filter == 0 || filter == 2)
|
|
for (const auto &record : published.friends) if (record.handle) next.values[next.count++] = record.handle;
|
|
const auto pointer = next.values.data();
|
|
n.publish(manager, handle, s.base + party::sorted_friends_type, &pointer);
|
|
}
|
|
bool party_list(Address manager, const Published &published) {
|
|
const auto root = singleton(manager, 0xf86b63a2, 104);
|
|
if (!root) return false;
|
|
auto &n = game::native_data().models; const auto base = state().base;
|
|
// The party's stable id (the local player's own Group).
|
|
std::uint64_t party_id{};
|
|
for (const auto &record : published.records)
|
|
if (record.handle && record.player.local) party_id = record.group.id;
|
|
publish_value(manager, field(manager, root, 2, 0xda8be596, 16), base + engine::uint64_type, party_id);
|
|
const auto list = field(manager, root, 0, 0xeec2d797, 0);
|
|
Address rows{}; std::uint32_t count{};
|
|
using Row = std::array<std::byte, 112>;
|
|
const auto value = n.value(manager, list, 0, 0);
|
|
// Social records, map markers and our multiplayer roster cover the full network membership.
|
|
require(value && game::native_array<Row>(reinterpret_cast<const void *>(value), rows, count, authored_party_slots) &&
|
|
count == authored_party_slots, "Native party requires its authored eight member slots.");
|
|
// Do not replace an unrelated native party or its callbacks. Only empty
|
|
// slots and records previously projected by this adapter are eligible.
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
const auto id = read<std::uint64_t>(rows + i * 112 + 72);
|
|
require(!id || state().owned_ids.contains(id), "An existing native party owns the party menu.");
|
|
}
|
|
// Party members only (the roster lists them first, leader first); the rest of the
|
|
// session are other players, not the party.
|
|
std::array<const Record *, authored_party_slots> members{};
|
|
unsigned used{};
|
|
for (const auto &record : published.records)
|
|
if (record.handle && record.player.member && used < members.size()) members[used++] = &record;
|
|
const Record empty;
|
|
for (unsigned i = 0; i < count; ++i) {
|
|
const auto member = n.field(manager, list, UINT32_MAX, i, false);
|
|
const auto type = model_type(manager, member);
|
|
require(type && read<std::uint32_t>(read<Address>(type)) == 0xd0ffd455 &&
|
|
read<std::uint16_t>(read<Address>(type) + 6) == 112,
|
|
"Native party member element differs.");
|
|
const auto &record = i < used ? *members[i] : empty;
|
|
const NativeText name(record.handle ? record.player.name : "");
|
|
publish_value(manager, field(manager, member, 1, 0x1011d206, 48), base + engine::string_type, name.value);
|
|
publish_value(manager, field(manager, member, 3, 0x94877505, 64), base + engine::bool_type, false);
|
|
publish_value(manager, field(manager, member, 4, 0xb9c67172, 65), base + engine::bool_type, record.player.local);
|
|
publish_value(manager, field(manager, member, 5, 0x3bd2a3cb, 72), base + engine::persona_id_type, record.player.id);
|
|
publish_value(manager, field(manager, member, 6, 0x81d5aa57, 80), base + engine::uint64_type, record.handle);
|
|
publish_value(manager, field(manager, member, 7, 0x9d501f88, 88), base + engine::bool_type, false);
|
|
}
|
|
return true;
|
|
}
|
|
// A field of a model, published only while this build's schema has it where expected (else skipped).
|
|
template<class T> bool publish_field(Address manager, Handle parent, unsigned index, std::uint32_t hash,
|
|
std::uint16_t offset, const T &value) {
|
|
const auto type = model_type(manager, parent);
|
|
if (!type) return false;
|
|
const auto meta = read<Address>(type);
|
|
if (index >= read<std::uint16_t>(meta + 0x2a)) return false;
|
|
const auto entry = read<Address>(meta + 0x60) + index * 24;
|
|
if (read<std::uint32_t>(entry) != hash || read<std::uint16_t>(entry + 8) != offset) return false;
|
|
const auto child = game::native_data().models.field(manager, parent, index, UINT32_MAX, false);
|
|
const auto child_type = read<Address>(entry + 16);
|
|
if (!child || model_type(manager, child) != child_type) return false;
|
|
game::native_data().models.publish(manager, child, child_type, &value);
|
|
return true;
|
|
}
|
|
// The local player's own UIPlayerInfo is the game's: its Profile.Group is published field by field
|
|
// (our records carry theirs in the record itself, player_record).
|
|
void party_profile(Address manager, Handle info, const GroupStatus &group) {
|
|
if (!info || model_type(manager, info) != state().base + engine::player_info_type) return;
|
|
const auto base = state().base;
|
|
const auto profile = field(manager, info, 1, 0x8f991be2, 0);
|
|
const auto g = field(manager, profile, 8, 0x0c470c21, 0);
|
|
// IsPartyLeader_Predicate reads UIPlayerInfo.Profile.Group.MemberCount
|
|
// and IsLeader. IsPartyMember alone only decorates the roster row.
|
|
publish_value(manager, field(manager, g, 2, 0xbed2dcf6, 8), base + engine::uint64_type, group.id);
|
|
publish_value(manager, field(manager, g, 3, 0x33d68284, 16), base + party::uint8_type, group.members);
|
|
// MaxMemberCount drives the "members / limit" party indicator and the Full checks: the party's cap.
|
|
publish_value(manager, field(manager, g, 4, 0xd6604a2a, 17), base + party::uint8_type, group.limit);
|
|
publish_value(manager, field(manager, g, 5, 0xc261d182, 18), base + engine::bool_type, group.leader);
|
|
publish_value(manager, field(manager, g, 7, 0x9bcfd36c, 24), base + engine::persona_id_type, group.leader_id);
|
|
// Join / invite permission and "parties unlocked" (ui-actions.md 1.2); skipped if moved.
|
|
publish_field(manager, g, 0, 0xed3e9214, 0, group.join);
|
|
publish_field(manager, g, 1, 0xb0c50f57, 4, group.invite);
|
|
publish_field(manager, g, 8, 0xa3e14bc2, 32, group.unlocked);
|
|
publish_value(manager, field(manager, info, 18, 0x11cca848, 257), base + engine::bool_type, group.members > 1);
|
|
}
|
|
} // namespace
|
|
void update_native_party(std::uintptr_t base, const PartyRoster &roster, unsigned capacity,
|
|
bool overlay) noexcept {
|
|
auto &s = state();
|
|
try {
|
|
const bool active = std::any_of(roster.begin(), roster.end(), [](const auto &p) { return p.id != 0; });
|
|
const auto steam = steam_social_snapshot();
|
|
if (!active && !s.manager && steam->friends.empty()) return;
|
|
if (!s.attempted) install(base);
|
|
if (!s.installed.load(std::memory_order_acquire)) return;
|
|
const auto now = GetTickCount64();
|
|
auto previous = s.published.load(std::memory_order_acquire);
|
|
bool changed = !previous || previous->social_revision != steam->revision ||
|
|
previous->capacity != capacity || previous->overlay != overlay;
|
|
bool refreshed{}, talking{};
|
|
if (previous) for (std::size_t i = 0; i < roster.size(); ++i) {
|
|
const auto &old = previous->records[i].player;
|
|
changed |= roster[i].id != old.id || roster[i].epoch != old.epoch || roster[i].name != old.name ||
|
|
roster[i].present != old.present || roster[i].leader != old.leader || roster[i].card != old.card ||
|
|
roster[i].member != old.member || roster[i].party != old.party ||
|
|
roster[i].leader_of_party != old.leader_of_party;
|
|
// Not a native change, but the lookups and markers find players by these.
|
|
refreshed |= roster[i].slot != old.slot || roster[i].local != old.local;
|
|
talking |= roster[i].mic != old.mic;
|
|
}
|
|
const auto ui = read<Address>(base + engine::ui_manager);
|
|
const auto manager = ui ? read<Address>(ui + 0x140) : 0;
|
|
// Publishing reaches native observers and rewrites the social lists under
|
|
// the model lock: only on a change (roster, Steam friends, limits or a new
|
|
// UI model manager), plus a slow re-publish restoring anything the game
|
|
// replaced in its own lists meanwhile.
|
|
refreshed |= previous && previous->manager != manager;
|
|
// A new local player card also needs the party fields.
|
|
const auto local_info = profile_runtime::player_card_runtime().functions.get_local_info
|
|
? profile_runtime::local_player_info_hook() : 0;
|
|
refreshed |= previous && local_info != s.local_info;
|
|
if (!changed && !refreshed && !talking && now < s.next_update) return;
|
|
const auto ui_early = read<Address>(base + engine::ui_manager);
|
|
const auto manager_early = ui_early ? read<Address>(ui_early + 0x140) : 0;
|
|
if (!changed && !refreshed && talking && previous && manager_early && previous->manager == manager_early) {
|
|
// Only who is talking changed: republish those players' records, nothing else.
|
|
game::ModelWriteLock lock(manager_early);
|
|
auto next = std::make_shared<Published>(*previous);
|
|
for (std::size_t i = 0; i < roster.size(); ++i) {
|
|
auto &record = next->records[i];
|
|
if (!record.handle || record.player.mic == roster[i].mic) continue;
|
|
record.player.mic = roster[i].mic;
|
|
record.handle = player_record(manager_early, record.player, record.player_id, record.player.member,
|
|
record.friend_of, true, record.colour, record.group);
|
|
}
|
|
next->index();
|
|
s.published.store(next, std::memory_order_release);
|
|
return;
|
|
}
|
|
// The party menu's assets load when it is first opened: until its slots are
|
|
// bound, try again soon.
|
|
s.next_update = now + (s.party_ready ? 5000 : 500);
|
|
if (!manager) { s.published.store({}); s.roster_active = false; return; }
|
|
const auto native_client = read<Address>(base + party::party_client);
|
|
if (native_client && read<Address>(native_client + 0x70)) {
|
|
s.published.store({}); s.roster_active = false;
|
|
throw std::runtime_error("An existing native EA party owns the social UI.");
|
|
}
|
|
{
|
|
game::ModelWriteLock lock(manager);
|
|
if (manager != s.manager) {
|
|
s.manager = manager; s.handles.clear(); s.owned.clear(); s.owned_ids.clear();
|
|
s.published.store({});
|
|
}
|
|
auto next = std::make_shared<Published>(); next->manager = manager; next->social_revision = steam->revision;
|
|
next->capacity = capacity; next->overlay = overlay;
|
|
std::int32_t colours{};
|
|
for (std::size_t i = 0; i < roster.size(); ++i) {
|
|
if (!roster[i].id) continue;
|
|
auto &record = next->records[i]; record.player = roster[i];
|
|
record.player_id = 0x7e000000U + static_cast<std::uint32_t>(i);
|
|
// The game's colour list is sized for its eight-member parties: a bigger one repeats them.
|
|
record.colour = roster[i].member ? colours++ % static_cast<std::int32_t>(authored_party_slots) : -1;
|
|
record.friend_of = std::any_of(steam->friends.begin(), steam->friends.end(), [&](const auto &friend_info) {
|
|
return friend_info.id == roster[i].id;
|
|
});
|
|
record.group = group_status(roster, capacity, roster[i], overlay || !roster[i].member);
|
|
const auto &old = previous ? previous->records[i] : Record{};
|
|
if (previous && previous->manager == manager && old.handle &&
|
|
old.player.id == roster[i].id && old.player.epoch == roster[i].epoch &&
|
|
old.player.name == roster[i].name && old.player.present == roster[i].present &&
|
|
old.player.local == roster[i].local && old.player.card == roster[i].card &&
|
|
old.player.member == roster[i].member && old.player.mic == roster[i].mic &&
|
|
old.colour == record.colour && old.friend_of == record.friend_of && old.group == record.group &&
|
|
old.player.party_open == roster[i].party_open &&
|
|
previous->social_revision == steam->revision &&
|
|
game::native_data().models.value(manager, old.handle, 0, 0))
|
|
record.handle = old.handle;
|
|
else record.handle = player_record(manager, roster[i], record.player_id, roster[i].member,
|
|
record.friend_of, true, record.colour, record.group);
|
|
}
|
|
if (previous && previous->manager == manager && previous->social_revision == steam->revision && !changed)
|
|
next->friends = previous->friends;
|
|
else for (const auto &friend_info : steam->friends) {
|
|
const auto member = std::find_if(next->records.begin(), next->records.end(), [&](const auto &r) {
|
|
return r.handle && r.player.id == friend_info.id;
|
|
});
|
|
if (member != next->records.end()) next->friends.push_back(*member);
|
|
else {
|
|
Record record;
|
|
record.player.id = friend_info.id; record.player.name = friend_info.name;
|
|
// Not in the session: nobody can invite them to a party here.
|
|
GroupStatus away;
|
|
away.invite = 0;
|
|
record.handle = player_record(manager, record.player, 0, false, true, friend_info.online, -1, away);
|
|
next->friends.push_back(std::move(record));
|
|
}
|
|
}
|
|
// Make the identity lookup available before publishing arrays that can
|
|
// immediately invoke native UI observers.
|
|
next->index();
|
|
s.published.store(next, std::memory_order_release);
|
|
s.roster_active.store(active, std::memory_order_release);
|
|
GroupStatus local_group;
|
|
for (const auto &record : next->records)
|
|
if (record.handle && record.player.local) local_group = record.group;
|
|
party_profile(manager, local_info, local_group);
|
|
social_list(ui, manager, *next);
|
|
social_list(ui, manager, *next, true);
|
|
friends_view(manager, *next);
|
|
const bool party_ready = party_list(manager, *next);
|
|
s.party_ready = party_ready;
|
|
s.local_info = local_info;
|
|
const auto count = std::count_if(next->records.begin(), next->records.end(), [](const auto &r) { return r.handle != 0; });
|
|
const auto members = std::count_if(next->records.begin(), next->records.end(),
|
|
[](const auto &r) { return r.handle != 0 && r.player.member; });
|
|
status(!active ? "Native party: disconnected." : "Native party: " + std::to_string(count) + " players, " +
|
|
std::to_string(members) + " in your party; social list bound; " +
|
|
(party_ready ? "party slots bound." : "waiting for party menu assets."));
|
|
}
|
|
// The game's own roster observers populate dependent native views.
|
|
// Dispatch after releasing the model lock and only on roster changes.
|
|
if (changed) {
|
|
reinterpret_cast<void (*)()>(base + party::roster_changed)();
|
|
// The party notifications (PartyService rebuilds its rows from them, without our names:
|
|
// republish shortly after).
|
|
const auto published = s.published.load(std::memory_order_acquire);
|
|
if (published) {
|
|
post_party_changes(previous && previous->manager == published->manager ? previous.get() : nullptr, *published);
|
|
s.next_update = std::min<ULONGLONG>(s.next_update, GetTickCount64() + 300);
|
|
}
|
|
}
|
|
} catch (const std::exception &e) { status(e.what()); }
|
|
}
|
|
void update_party_position(const Pose *pose) noexcept {
|
|
auto &s = state(); std::lock_guard lock(s.position_mutex);
|
|
auto &position = s.positions[peer_slot];
|
|
// Playback poses were fully validated when admitted to PoseBuffer. This
|
|
// adapter consumes only the root, so do not walk another 412 transforms.
|
|
position.valid = pose && valid_transform(pose->root); position.sampled = GetTickCount64();
|
|
if (position.valid) position.root = pose->root;
|
|
}
|
|
std::uint64_t native_party_player_info(std::uintptr_t manager, std::size_t slot) noexcept {
|
|
try {
|
|
const auto published = state().published.load(std::memory_order_acquire);
|
|
if (!published || published->manager != manager) return 0;
|
|
if (const auto record = published->slot_record(slot)) {
|
|
game::ModelWriteLock lock(manager);
|
|
return game::native_data().models.value(manager, record->handle, 0, 0) ? record->handle : 0;
|
|
}
|
|
} catch (...) {}
|
|
return 0;
|
|
}
|
|
std::string native_party_status() {
|
|
auto &s = state(); std::lock_guard lock(s.status_mutex);
|
|
return s.status + " Map party markers: " + std::to_string(s.map_count.load()) + ".";
|
|
}
|
|
void set_native_party_map_markers(bool enabled) noexcept { state().map_markers.store(enabled, std::memory_order_release); }
|
|
bool native_party_map_markers() noexcept { return state().map_markers.load(std::memory_order_acquire); }
|
|
bool native_party_map_icon_visible(std::size_t slot) noexcept {
|
|
return slot < max_remote_players && ((state().map_slots[slot / 64].load() >> (slot % 64)) & 1) != 0;
|
|
}
|
|
} // namespace dingosdk::multiplayer
|