mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 03:47:01 +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.
434 lines
22 KiB
C++
434 lines
22 KiB
C++
#include "native_party_internal.h"
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Extension/Multiplayer/Session/session.h"
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <cstring>
|
|
#include <format>
|
|
|
|
// The game's own party buttons (Social menu, player cards, invites) end in a handful of request
|
|
// natives that need a native Blaze game group, which ReSkate never has. While the SDK owns the
|
|
// party, these hooks turn each request into a ReSkate party request (the session's "party"
|
|
// command, which whoever hosts answers) and complete the button's callback themselves.
|
|
// Party queries are answered from the published roster (analysis/party-re/native-core.md).
|
|
namespace dingosdk::multiplayer {
|
|
using namespace native_party_detail;
|
|
namespace {
|
|
using Delegate = Address;
|
|
struct Requests {
|
|
Address (*invite)(Address, const std::uint64_t *, Delegate){};
|
|
Address (*join_group)(Address, std::uint64_t, std::int32_t, Delegate){};
|
|
Address (*join_player)(Address, const std::uint64_t *, Delegate){};
|
|
Address (*decline)(Address, const Address *, Delegate){};
|
|
Address (*leave)(Address, Delegate){};
|
|
Address (*kick)(Address, const std::uint64_t *, Delegate){};
|
|
Address (*promote)(Address, const std::uint64_t *, Delegate){};
|
|
void (*privacy)(std::uint32_t, std::uint32_t){};
|
|
void (*local_is_leader)(bool *){};
|
|
bool (*in_group)(){};
|
|
void (*group_status)(const std::uint64_t *, void *, bool *){};
|
|
Address (*accept_invite)(Address, const Address *, const Address *, Delegate){};
|
|
bool (*voice_unavailable)(){};
|
|
std::atomic<bool> installed{};
|
|
// Accepting an invite also clears it (DeleteInvite): that is no decline.
|
|
std::mutex mutex;
|
|
std::map<std::uint64_t, std::uint64_t> accepted; // inviter -> when (ms)
|
|
std::map<std::uint64_t, std::uint64_t> invited; // player -> when the local player invited them (ms)
|
|
};
|
|
Requests &requests() { static auto *value = new Requests; return *value; }
|
|
std::atomic<bool> changes_allowed{};
|
|
constexpr const char *lobby_party = "Parties need a multiplayer session.";
|
|
std::uint64_t now_ms() {
|
|
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
}
|
|
// The SDK answers only while it owns the party: a session roster is published and the game
|
|
// has no native group of its own.
|
|
std::shared_ptr<const Published> owned_party() {
|
|
auto &s = state();
|
|
if (!s.installed.load(std::memory_order_acquire) || !s.roster_active.load(std::memory_order_acquire)) return {};
|
|
const auto native_client = read<Address>(s.base + party::party_client);
|
|
if (native_client && read<Address>(native_client + 0x70)) return {};
|
|
return s.published.load(std::memory_order_acquire);
|
|
}
|
|
const Record *local_record(const Published &p) {
|
|
for (const auto &record : p.records)
|
|
if (record.handle && record.player.local) return &record;
|
|
return nullptr;
|
|
}
|
|
// A callback to answer: some requests pass an empty delegate (the invite toast's reject passes
|
|
// none), which the natives skip ([delegate] == 0) and so must we.
|
|
bool has_callback(Delegate callback) {
|
|
Address delegate{};
|
|
return callback && memory::peek(callback, delegate) && delegate;
|
|
}
|
|
// Answers the button's callback now: code 0 is success; anything else shows as a failure.
|
|
Address complete(Address out, Delegate callback, std::uint32_t code = 0, const std::string &text = {}) {
|
|
const auto base = state().base;
|
|
alignas(16) std::array<std::byte, 0x40> result{};
|
|
reinterpret_cast<void (*)(void *, std::uint32_t, const char *)>(base + party::result_make)(result.data(), code, text.c_str());
|
|
if (has_callback(callback)) reinterpret_cast<void (*)(Delegate, void *)>(base + party::delegate_invoke)(callback, result.data());
|
|
reinterpret_cast<void (*)(void *)>(base + engine::native_text_release)(result.data());
|
|
if (out) reinterpret_cast<void (*)(Address, Address)>(base + party::null_handle)(out, 0);
|
|
return out;
|
|
}
|
|
// A code the Social callbacks treat as "no current group" (0xd5e8a544) with our own text.
|
|
constexpr std::uint32_t refused = 0xd5e8a544;
|
|
bool send(std::string_view verb, std::uint64_t player = 0) {
|
|
std::string argument(verb);
|
|
if (player) argument += " " + std::to_string(player);
|
|
const bool queued = queue_command("party", argument, {});
|
|
if (queued) logging::log(logging::Level::info, logging::Channel::ui, "Native party request: party {}.", argument);
|
|
return queued;
|
|
}
|
|
bool session_player(const Published &p, std::uint64_t id) {
|
|
const auto record = p.record(id);
|
|
return record && !record->player.local;
|
|
}
|
|
|
|
Address invite_hook(Address out, const std::uint64_t *eaid, Delegate callback) {
|
|
try {
|
|
if (const auto p = owned_party(); p && eaid && session_player(*p, *eaid)) {
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
{
|
|
std::lock_guard lock(requests().mutex);
|
|
requests().invited[*eaid] = now_ms();
|
|
}
|
|
if (!send("invite", *eaid)) return complete(out, callback, refused, "Could not send the invite.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().invite(out, eaid, callback);
|
|
}
|
|
// Accepting an invite (reason 2) names the group the invite carried, which the SDK sets to the
|
|
// inviter's Steam ID; a direct join (1) names the party's leader the same way.
|
|
// The session player a group id names: an invite's GameId is the inviter's Steam ID; a party's
|
|
// group id (native_party_group_id) names that party, joined through its leader.
|
|
std::uint64_t group_player(const Published &p, std::uint64_t group) {
|
|
if (session_player(p, group)) return group;
|
|
for (const auto &record : p.records)
|
|
if (record.handle && !record.player.local && record.group.id == group && record.group.leader_id &&
|
|
session_player(p, record.group.leader_id))
|
|
return record.group.leader_id;
|
|
return 0;
|
|
}
|
|
bool accepted_recently(std::uint64_t inviter) {
|
|
auto &r = requests();
|
|
std::lock_guard lock(r.mutex);
|
|
const auto it = r.accepted.find(inviter);
|
|
return it != r.accepted.end() && now_ms() - it->second < 10000;
|
|
}
|
|
Address join_group_hook(Address out, std::uint64_t group, std::int32_t reason, Delegate callback) {
|
|
try {
|
|
const auto p = owned_party();
|
|
if (const auto player = p ? group_player(*p, group) : 0) {
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
// The toast's Accept already asked the server (accept_invite_hook); this is its follow-up.
|
|
if (reason == 2 && accepted_recently(player)) return complete(out, callback);
|
|
if (reason == 2) {
|
|
std::lock_guard lock(requests().mutex);
|
|
requests().accepted[player] = now_ms();
|
|
}
|
|
if (!send(reason == 2 ? "accept" : "join", player)) return complete(out, callback, refused, "Could not reach the server.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().join_group(out, group, reason, callback);
|
|
}
|
|
std::uint64_t string_id(const Address *text) {
|
|
Address chars{};
|
|
if (!text || !memory::peek(reinterpret_cast<Address>(text), chars) || !chars) return 0;
|
|
std::array<char, 32> buffer{};
|
|
memory::peek_bytes(chars, buffer.data(), buffer.size() - 1);
|
|
const std::string_view value(buffer.data());
|
|
std::uint64_t id{};
|
|
const auto parsed = std::from_chars(value.data(), value.data() + value.size(), id);
|
|
return parsed.ec == std::errc{} && parsed.ptr == value.data() + value.size() ? id : 0;
|
|
}
|
|
// The invite toast's Accept: (Handle*, invite id, game id, Delegate(error, game id)). Both ids are
|
|
// the inviter's Steam ID (post_native_party_invite).
|
|
Address accept_invite_hook(Address out, const Address *invite, const Address *game, Delegate callback) {
|
|
try {
|
|
const auto p = owned_party();
|
|
const auto inviter = string_id(invite);
|
|
if (p && inviter && session_player(*p, inviter)) {
|
|
const auto base = state().base;
|
|
alignas(16) std::array<std::byte, 0x40> result{};
|
|
const bool allowed = changes_allowed.load(std::memory_order_acquire);
|
|
bool sent{};
|
|
if (allowed) {
|
|
{
|
|
std::lock_guard lock(requests().mutex);
|
|
requests().accepted[inviter] = now_ms();
|
|
}
|
|
sent = send("accept", inviter);
|
|
}
|
|
reinterpret_cast<void (*)(void *, std::uint32_t, const char *)>(base + party::result_make)(
|
|
result.data(), sent ? 0 : refused, sent ? "" : allowed ? "Could not reach the server." : lobby_party);
|
|
if (has_callback(callback))
|
|
reinterpret_cast<void (*)(Delegate, void *, const Address *)>(base + party::delegate_invoke_text)(
|
|
callback, result.data(), game);
|
|
reinterpret_cast<void (*)(void *)>(base + engine::native_text_release)(result.data());
|
|
if (out) reinterpret_cast<void (*)(Address, Address)>(base + party::null_handle)(out, 0);
|
|
return out;
|
|
}
|
|
} catch (...) {}
|
|
return requests().accept_invite(out, invite, game, callback);
|
|
}
|
|
// Party voice options show while the local player is in a ReSkate party.
|
|
bool voice_unavailable_hook() {
|
|
const bool native = requests().voice_unavailable();
|
|
try {
|
|
if (native)
|
|
if (const auto p = owned_party()) {
|
|
const auto local = local_record(*p);
|
|
if (local && local->player.member && local->group.members > 1) return false;
|
|
}
|
|
} catch (...) {}
|
|
return native;
|
|
}
|
|
Address join_player_hook(Address out, const std::uint64_t *eaid, Delegate callback) {
|
|
try {
|
|
if (const auto p = owned_party(); p && eaid && session_player(*p, *eaid)) {
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
if (!send("join", *eaid)) return complete(out, callback, refused, "Could not reach the server.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().join_player(out, eaid, callback);
|
|
}
|
|
Address decline_hook(Address out, const Address *invite, Delegate callback) {
|
|
try {
|
|
Address text{};
|
|
if (const auto p = owned_party(); p && invite && memory::peek(reinterpret_cast<Address>(invite), text) && text) {
|
|
std::array<char, 32> chars{};
|
|
memory::peek_bytes(text, chars.data(), chars.size() - 1);
|
|
std::uint64_t from{};
|
|
const std::string_view id(chars.data());
|
|
const auto parsed = std::from_chars(id.data(), id.data() + id.size(), from);
|
|
if (parsed.ec == std::errc{} && parsed.ptr == id.data() + id.size() && session_player(*p, from)) {
|
|
bool just_accepted{};
|
|
{
|
|
auto &r = requests();
|
|
std::lock_guard lock(r.mutex);
|
|
const auto it = r.accepted.find(from);
|
|
just_accepted = it != r.accepted.end() && now_ms() - it->second < 10000;
|
|
if (it != r.accepted.end()) r.accepted.erase(it);
|
|
}
|
|
if (!just_accepted) send("decline", from);
|
|
return complete(out, callback);
|
|
}
|
|
}
|
|
} catch (...) {}
|
|
return requests().decline(out, invite, callback);
|
|
}
|
|
Address leave_hook(Address out, Delegate callback) {
|
|
try {
|
|
if (const auto p = owned_party()) {
|
|
const auto local = local_record(*p);
|
|
if (!local || !local->player.member) return complete(out, callback, refused, "You're not in a party.");
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
if (!send("leave")) return complete(out, callback, refused, "Could not reach the server.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().leave(out, callback);
|
|
}
|
|
Address kick_hook(Address out, const std::uint64_t *eaid, Delegate callback) {
|
|
try {
|
|
if (const auto p = owned_party(); p && eaid && session_player(*p, *eaid)) {
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
if (!send("kick", *eaid)) return complete(out, callback, refused, "Could not reach the server.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().kick(out, eaid, callback);
|
|
}
|
|
Address promote_hook(Address out, const std::uint64_t *eaid, Delegate callback) {
|
|
try {
|
|
if (const auto p = owned_party(); p && eaid && session_player(*p, *eaid)) {
|
|
if (!changes_allowed.load(std::memory_order_acquire)) return complete(out, callback, refused, lobby_party);
|
|
if (!send("promote", *eaid)) return complete(out, callback, refused, "Could not reach the server.");
|
|
return complete(out, callback);
|
|
}
|
|
} catch (...) {}
|
|
return requests().promote(out, eaid, callback);
|
|
}
|
|
// Settings 4 (who may invite) and 5 (who may join directly): "Anyone" joining opens the party,
|
|
// anything else makes it invite-only. The game keeps its own copy of the setting too.
|
|
void privacy_hook(std::uint32_t setting, std::uint32_t value) {
|
|
requests().privacy(setting, value);
|
|
try {
|
|
if (setting != 5) return;
|
|
const auto p = owned_party();
|
|
const auto local = p ? local_record(*p) : nullptr;
|
|
if (local && local->player.member && local->player.leader && changes_allowed.load(std::memory_order_acquire))
|
|
send((value & 1) ? "open" : "close");
|
|
} catch (...) {}
|
|
}
|
|
void local_is_leader_hook(bool *out) {
|
|
requests().local_is_leader(out);
|
|
try {
|
|
if (!out || *out) return;
|
|
if (const auto p = owned_party()) {
|
|
const auto local = local_record(*p);
|
|
*out = local && local->player.member && local->player.leader;
|
|
}
|
|
} catch (...) {}
|
|
}
|
|
bool in_group_hook() {
|
|
if (requests().in_group()) return true;
|
|
try {
|
|
if (const auto p = owned_party()) {
|
|
const auto local = local_record(*p);
|
|
return local && local->player.member;
|
|
}
|
|
} catch (...) {}
|
|
return false;
|
|
}
|
|
// A player's GameGroupStatus (0x28 bytes: +8 group id, +0x10 member count, +0x11 limit,
|
|
// +0x12 is leader, +0x18 leader id): for a session player, their party as the roster says.
|
|
void group_status_hook(const std::uint64_t *eaid, void *out, bool *found) {
|
|
requests().group_status(eaid, out, found);
|
|
try {
|
|
if (!eaid || !out || !found || *found) return;
|
|
const auto p = owned_party();
|
|
const auto record = p ? p->record(*eaid) : nullptr;
|
|
if (!record || !record->group.id) return;
|
|
const auto &g = record->group;
|
|
std::array<std::byte, 0x28> status{};
|
|
const auto put = [&](std::size_t at, auto value) { std::memcpy(status.data() + at, &value, sizeof(value)); };
|
|
put(0x00, g.join); put(0x04, g.invite); put(0x08, g.id); put(0x10, g.members); put(0x11, g.limit);
|
|
put(0x12, g.leader); put(0x18, g.leader_id); put(0x20, g.unlocked);
|
|
std::memcpy(out, status.data(), status.size());
|
|
*found = true;
|
|
} catch (...) {}
|
|
}
|
|
} // namespace
|
|
|
|
void set_native_party_changes(bool allowed) noexcept { changes_allowed.store(allowed, std::memory_order_release); }
|
|
|
|
namespace native_party_detail {
|
|
std::uint64_t last_native_invite(std::uint64_t id) {
|
|
auto &r = requests();
|
|
std::lock_guard lock(r.mutex);
|
|
const auto it = r.invited.find(id);
|
|
return it == r.invited.end() ? 0 : it->second;
|
|
}
|
|
// Raises a native UI event as the natives do (ui-actions.md 1.6); the payload is copied by the
|
|
// dispatcher's type-aware copy, so it may be freed after. Game thread.
|
|
bool post_event(std::uintptr_t type, const void *payload) {
|
|
const auto base = state().base;
|
|
alignas(16) std::array<std::byte, 16> context{};
|
|
reinterpret_cast<void (*)(void *)>(base + party::current_context)(context.data());
|
|
const auto dispatcher = reinterpret_cast<Address (*)(void *)>(base + party::event_dispatcher)(context.data());
|
|
if (!dispatcher || !read<std::uint8_t>(dispatcher + 0x28)) return false;
|
|
struct Options { float delay; std::uint32_t count; std::uint32_t flags; } options{0.f, 1, 0};
|
|
reinterpret_cast<void (*)(Address, Address, const void *, void *, Address)>(base + party::event_post)(
|
|
dispatcher, base + type, payload, &options, 0);
|
|
return true;
|
|
}
|
|
void post_party_changes(const Published *previous, const Published &next) {
|
|
if (!requests().installed.load(std::memory_order_acquire)) return;
|
|
// The local party as the game shows it.
|
|
const auto members = [](const Published *p) {
|
|
std::map<std::uint64_t, bool> result; // id -> leads
|
|
if (!p) return result;
|
|
const Record *local{};
|
|
for (const auto &record : p->records)
|
|
if (record.handle && record.player.local) local = &record;
|
|
if (!local || !local->group.id) return result;
|
|
for (const auto &record : p->records)
|
|
if (record.handle && record.player.member && record.group.id == local->group.id)
|
|
result[record.player.id] = record.player.leader_of_party;
|
|
return result;
|
|
};
|
|
const auto before = members(previous), after = members(&next);
|
|
if (before == after) return;
|
|
std::uint64_t local{};
|
|
for (const auto &record : next.records)
|
|
if (record.handle && record.player.local) local = record.player.id;
|
|
struct Leave { std::uint64_t id; std::int32_t reason; std::int32_t padding; };
|
|
// The local player joined a party (or it formed around them): one notification for them.
|
|
if (before.empty() || !before.contains(local)) {
|
|
if (after.contains(local)) post_event(party::group_join_event, &local);
|
|
} else {
|
|
for (const auto &[id, leads] : after)
|
|
if (!before.contains(id)) post_event(party::group_join_event, &id);
|
|
}
|
|
if (!before.empty() && after.empty()) {
|
|
const Leave left{local, 0, 0};
|
|
post_event(party::group_leave_event, &left);
|
|
} else {
|
|
for (const auto &[id, leads] : before)
|
|
if (!after.contains(id)) {
|
|
const Leave left{id, 0, 0};
|
|
post_event(party::group_leave_event, &left);
|
|
}
|
|
}
|
|
for (const auto &[id, leads] : after)
|
|
if (leads && before.contains(id) && !before.at(id)) post_event(party::leader_changed_event, &id);
|
|
}
|
|
} // namespace native_party_detail
|
|
|
|
bool post_native_party_invite(std::uint64_t from) noexcept {
|
|
try {
|
|
const auto p = owned_party();
|
|
// The toast needs the inviter's player record, or it declines the invite at once.
|
|
if (!p || !requests().installed.load(std::memory_order_acquire) || !session_player(*p, from)) return false;
|
|
const auto id = std::to_string(from);
|
|
const NativeText game(id), invite(id);
|
|
struct Payload {
|
|
Address game, invite;
|
|
std::uint64_t inviter;
|
|
std::int32_t type;
|
|
bool matchmaking;
|
|
} payload{game.value, invite.value, from, 1, false};
|
|
static_assert(sizeof(Payload) == 0x20);
|
|
return post_event(party::game_invite_event, &payload);
|
|
} catch (...) { return false; }
|
|
}
|
|
|
|
namespace native_party_detail {
|
|
// Separate from install(): a build whose request natives moved keeps the other adapters.
|
|
void install_requests(Address base) {
|
|
auto &r = requests();
|
|
for (const auto &contract : party::party_request_contracts) {
|
|
std::array<unsigned char, 32> bytes{};
|
|
require(memory::read(base + contract.rva, bytes) && bytes == contract.bytes,
|
|
"Native party request fingerprint differs; the game's party buttons stay native.");
|
|
}
|
|
struct Hook { Address rva; void *replacement; void **original; };
|
|
const std::array hooks{
|
|
Hook{party::request_invite, reinterpret_cast<void *>(&invite_hook), reinterpret_cast<void **>(&r.invite)},
|
|
Hook{party::request_join_group, reinterpret_cast<void *>(&join_group_hook), reinterpret_cast<void **>(&r.join_group)},
|
|
Hook{party::request_join_player, reinterpret_cast<void *>(&join_player_hook), reinterpret_cast<void **>(&r.join_player)},
|
|
Hook{party::request_decline, reinterpret_cast<void *>(&decline_hook), reinterpret_cast<void **>(&r.decline)},
|
|
Hook{party::request_leave, reinterpret_cast<void *>(&leave_hook), reinterpret_cast<void **>(&r.leave)},
|
|
Hook{party::request_kick, reinterpret_cast<void *>(&kick_hook), reinterpret_cast<void **>(&r.kick)},
|
|
Hook{party::request_promote, reinterpret_cast<void *>(&promote_hook), reinterpret_cast<void **>(&r.promote)},
|
|
Hook{party::request_privacy, reinterpret_cast<void *>(&privacy_hook), reinterpret_cast<void **>(&r.privacy)},
|
|
Hook{party::local_is_leader, reinterpret_cast<void *>(&local_is_leader_hook), reinterpret_cast<void **>(&r.local_is_leader)},
|
|
Hook{party::in_group, reinterpret_cast<void *>(&in_group_hook), reinterpret_cast<void **>(&r.in_group)},
|
|
Hook{party::player_group_status, reinterpret_cast<void *>(&group_status_hook), reinterpret_cast<void **>(&r.group_status)},
|
|
Hook{party::request_accept_invite, reinterpret_cast<void *>(&accept_invite_hook), reinterpret_cast<void **>(&r.accept_invite)},
|
|
Hook{party::voice_unavailable, reinterpret_cast<void *>(&voice_unavailable_hook), reinterpret_cast<void **>(&r.voice_unavailable)}};
|
|
std::size_t prepared{};
|
|
for (const auto &hook : hooks) {
|
|
if (hook_prepare(reinterpret_cast<void *>(base + hook.rva), hook.replacement, hook.original) != HookOk) {
|
|
while (prepared) hook_remove(reinterpret_cast<void *>(base + hooks[--prepared].rva));
|
|
throw std::runtime_error("Cannot prepare the native party request hooks.");
|
|
}
|
|
++prepared;
|
|
}
|
|
for (const auto &hook : hooks)
|
|
require(hook_queue_enable(reinterpret_cast<void *>(base + hook.rva)) == HookOk, "Cannot enable the native party request hooks.");
|
|
require(hook_apply_queued() == HookOk, "Cannot enable the native party request hooks.");
|
|
r.installed.store(true, std::memory_order_release);
|
|
}
|
|
} // namespace native_party_detail
|
|
} // namespace dingosdk::multiplayer
|