mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 01:45:58 +02:00
Throwdowns: the throwdown kit, so a mode is a plugin and the SDK knows no particular mode
Plugins register modes (card, replaces, order, flag, players) and publish documents through reskate.throwdowns.ui: the setup page, the waiting card, HUD slots themed on the game's HUD_<theme>_* widgets, the match panel, flags and match restrictions. SmokeyDawg58's native plumbing (throwdown_menu.cpp and its .inl files) renders them and posts throwdown_ui events back. kit::flag is the one native flag registration. General services added for it: reskate.files, reskate.ui.dialog, reskate.skater push/hold/bone_cam, reskate.camera.pad_flight, reskate.physics competition, reskate.session.world, reskate.tricks feed/gesture, and world entities. His ten modes are now Lua in his plugin: the rules, referee and wire (td/rules, referee, wire), and every page and HUD document (td/ui*.lua). Extension/Modes and reskate.modes are gone, along with every race and 1-UP branch in the SDK. The plugin test drives each mode through kit events. A native setup page is never written to while it closes into flag placement: its rows are not republished, the stock page is put back once the exit is over, and the stock Players slider is left alone while a plugin flag is out. This fixes the crashes on the Deathrace button and on 1-UP's Confirm.
This commit is contained in:
1 parent
e14064250c
commit
8444553402
186 files changed
+28015
-2747
No files matched your search
@@ -1,5 +1,11 @@
|
||||
#include "native_throwdowns.h"
|
||||
#include "custom_placement.h"
|
||||
#include "native_throwdown_lifetime.h"
|
||||
#include "native_flag_session.h"
|
||||
#include "session_marker_actions.h"
|
||||
#include "throwdown_kit.h"
|
||||
#include "Extension/UI/NativeMenu/native_menu_data.h"
|
||||
#include "Extension/UI/NativeMenu/throwdown_menu.h"
|
||||
#include "Extension/UI/NativeMenu/native_hud_selection.h"
|
||||
#include "grouped_number.h"
|
||||
#include "throwdown_lab.h"
|
||||
#include "throwdown_relay.h"
|
||||
@@ -10,6 +16,7 @@
|
||||
#include "Extension/Profile/runtime_internal.h"
|
||||
#include "Extension/Progression/local_entitlement_trigger_runtime.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/supported_build.h"
|
||||
#include "Engine/Game/Build/20260929/engine.h"
|
||||
#include "Engine/Game/Build/20260929/native_party.h"
|
||||
#include "Engine/Game/Build/20260929/native_throwdowns.h"
|
||||
@@ -29,6 +36,7 @@ using Address = std::uintptr_t;
|
||||
namespace engine = addr::engine;
|
||||
namespace party = addr::native_party;
|
||||
namespace throwdowns = addr::native_throwdowns;
|
||||
namespace kit = dingosdk::throwdowns::kit;
|
||||
constexpr std::uint32_t client_realm = 0xbf0f9789, server_realm = 0x98be5555;
|
||||
struct State {
|
||||
Address base{};
|
||||
@@ -38,10 +46,21 @@ struct State {
|
||||
bool (*others_allowed)(){};
|
||||
std::uint64_t (*player_info)(std::uint32_t){};
|
||||
std::uint64_t (*service_player_info)(Address, std::uint32_t){};
|
||||
std::uint64_t (*player_in_activity)(std::uint32_t){};
|
||||
void (*send_event)(Address, Address, const Address*){};
|
||||
Address (*parameters)(Address, Address){};
|
||||
std::atomic<bool> throwdown_active{}; // hosting: created a throwdown
|
||||
std::atomic<bool> throwdown_joined{}; // in one someone else placed
|
||||
NativeThrowdownLifetime lifetime;
|
||||
NativeActivityUiLease custom_ui_presence;
|
||||
std::mutex custom_ui_presence_mutex;
|
||||
std::string custom_ui_diagnostic;
|
||||
std::uint64_t custom_ui_diagnostic_at{};
|
||||
std::atomic<Address> menu_trace_address{};
|
||||
std::atomic<DWORD> menu_trace_thread{};
|
||||
std::atomic<std::int32_t> menu_trace_selection{};
|
||||
std::atomic<bool> menu_details_presence_reported{};
|
||||
std::atomic<bool> menu_details_check_reported{};
|
||||
Address solo_client_activity{};
|
||||
std::uint32_t solo_client_player{}, solo_client_group{};
|
||||
};
|
||||
State& state() { static auto* value = new State; return *value; }
|
||||
struct PendingParameters { Address vm{}, callback{}; };
|
||||
@@ -61,26 +80,208 @@ std::uint32_t realm() {
|
||||
const auto offset = read<std::uint32_t>(state().base + engine::context_type_offset);
|
||||
return context && offset < 0x1000000 ? read<std::uint32_t>(context + offset) : 0;
|
||||
}
|
||||
// Native player id of the hosted offline server's only real player, or zero.
|
||||
// The MP participant id is not the PlayerId used by native client/UI getters.
|
||||
// Steam roster entries are UI models and never appear in this native list.
|
||||
std::uint32_t sole_offline_player_id() {
|
||||
NativeActivityPlayerIds sole_offline_player_ids() {
|
||||
const auto base = state().base;
|
||||
const auto server = read<Address>(base + engine::game_server);
|
||||
if (!server || read<Address>(server) != base + engine::server_vtable) return 0;
|
||||
if (!server || read<Address>(server) != base + engine::server_vtable) return {};
|
||||
const auto context = read<Address>(server + 8);
|
||||
const auto manager_offset = read<std::uint32_t>(base + engine::context_player_manager_offset);
|
||||
if (!context || manager_offset > 0x1000000) return 0;
|
||||
if (!context || manager_offset > 0x1000000) return {};
|
||||
const auto manager = read<Address>(context + manager_offset);
|
||||
if (!manager || read<Address>(manager) != base + engine::server_player_manager_vtable) return 0;
|
||||
if (!manager || read<Address>(manager) != base + engine::server_player_manager_vtable) return {};
|
||||
const auto first = read<Address>(manager + 0xc8), last = read<Address>(manager + 0xd0);
|
||||
if (!first || last - first != 8) return 0;
|
||||
if (!first || last - first != 8) return {};
|
||||
const auto player = read<Address>(first);
|
||||
// Registration offsets locate sibling base classes of the same player, so
|
||||
// the differences are signed and small.
|
||||
const auto parent = static_cast<std::int64_t>(read<std::uint32_t>(base + engine::player_parent_offset));
|
||||
const auto identity = static_cast<std::int64_t>(read<std::uint32_t>(base + throwdowns::player_identity_offset)) - parent;
|
||||
if (player < 0x100000 || identity < -0x10000 || identity > 0x10000) return 0;
|
||||
return read<std::uint32_t>(static_cast<Address>(static_cast<std::int64_t>(player) + identity) + 0x68);
|
||||
if (player < 0x100000 || identity < -0x10000 || identity > 0x10000 ||
|
||||
read<Address>(player)!=base+engine::server_player_vtable ||
|
||||
read<Address>(base+engine::server_player_vtable+0x10)!=base+0x7ff930)return {};
|
||||
// Native virtual GetPlayerId is mov eax,[rcx+48]; ret (IDA/Ghidra and
|
||||
// both live client/server vtables agree). Read it without a virtual call.
|
||||
static const bool id_verified=read<std::array<unsigned char,4>>(base+0x7ff930)==
|
||||
std::array<unsigned char,4>{0x8b,0x41,0x48,0xc3};
|
||||
if(!id_verified)return {};
|
||||
return {read<std::uint32_t>(static_cast<Address>(static_cast<std::int64_t>(player) + identity) + 0x68),
|
||||
read<std::uint32_t>(player+0x48)};
|
||||
}
|
||||
std::uint32_t sole_offline_player_id() { return sole_offline_player_ids().participant; }
|
||||
|
||||
// Same client-player lookup and sibling-base adjustment used by the native
|
||||
// GetPlayerIsInActivity/GetPlayerMPActivityIdleState functions (834520/834950).
|
||||
// Never use the skater or the SDK's UI record as a native player pointer.
|
||||
Address client_activity_variables(std::uint32_t participant,std::uint32_t* menu_player=nullptr) {
|
||||
const auto base=state().base;
|
||||
const auto ids=sole_offline_player_ids();
|
||||
if(!ids.matches_participant(participant))return 0;
|
||||
// Native GetPlayerIsInActivity selects the client realm explicitly with
|
||||
// GetContextForRealm, not the caller's TLS context. Our UI tick runs AFTER
|
||||
// original_tick returns, when that temporary TLS scope may already be gone.
|
||||
constexpr std::array<unsigned char,19> context_prefix{
|
||||
0x89,0x54,0x24,0x10,0x53,0x48,0x83,0xec,0x30,0x4c,0x8b,0x05,0xe8,0xb7,0x70,0x05,0x48,0x8b,0xd9};
|
||||
static const bool verified=read<std::array<unsigned char,19>>(base+0x18b6e40)==context_prefix;
|
||||
if(!verified)throw std::runtime_error("Native client context lookup fingerprint differs");
|
||||
Address context{};
|
||||
reinterpret_cast<Address (*)(Address*,std::uint32_t)>(base+0x18b6e40)(&context,client_realm);
|
||||
const auto realm_offset=read<std::uint32_t>(base+engine::context_type_offset);
|
||||
if(!context || realm_offset>0x1000000 || read<std::uint32_t>(context+realm_offset)!=client_realm)return 0;
|
||||
const auto offset=read<std::uint32_t>(base+engine::context_player_manager_offset);
|
||||
if(!context || offset>0x1000000)return 0;
|
||||
const auto manager=read<Address>(context+offset);
|
||||
if(!manager || read<Address>(manager)!=base+engine::local_player_manager_vtable)return 0;
|
||||
const auto first=read<Address>(manager+0x78),last=read<Address>(manager+0x80);
|
||||
if(!first || last<first || last-first>64*8 || (last-first)%8)return 0;
|
||||
const auto delta=static_cast<std::int64_t>(read<std::uint32_t>(base+0x6f7bae0))-
|
||||
static_cast<std::int64_t>(read<std::uint32_t>(base+0x75c3588));
|
||||
if(delta < -0x10000 || delta > 0x10000)return 0;
|
||||
for(auto at=first;at<last;at+=8) {
|
||||
const auto player=read<Address>(at);
|
||||
if(!player || read<Address>(player)!=base+engine::local_player_vtable)continue;
|
||||
const auto id_function=read<Address>(read<Address>(player)+0x10);
|
||||
if(id_function!=base+0x7ff930)continue;
|
||||
if(ids.matches_menu_player(read<std::uint32_t>(player+0x48))) {
|
||||
if(menu_player)*menu_player=ids.menu_player;
|
||||
return static_cast<Address>(static_cast<std::int64_t>(player)+delta);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void complete_solo_client_exit() {
|
||||
auto& s=state();
|
||||
if(!s.solo_client_activity && !s.lifetime.host())return;
|
||||
const auto uid=sole_offline_player_id();
|
||||
const auto activity=s.lifetime.solo_activity(uid);
|
||||
if(activity && activity!=s.solo_client_activity) {
|
||||
if(const auto vars=client_activity_variables(uid)) {
|
||||
s.solo_client_activity=activity;s.solo_client_player=uid;
|
||||
s.solo_client_group=read<std::uint32_t>(vars+0x100);
|
||||
}
|
||||
}
|
||||
const auto ended=s.lifetime.take_completed_solo();
|
||||
if(!ended)return;
|
||||
// A new queue or a changed group must not inherit this completed event's
|
||||
// cleanup. The server's confirmed removal/client EnterEnd arms it once.
|
||||
const auto vars=ended==uid && ended==s.solo_client_player && s.solo_client_activity &&
|
||||
!s.lifetime.active() && !native_flag::registration_owned()?client_activity_variables(ended):0;
|
||||
const auto group=vars?read<std::uint32_t>(vars+0x100):0;
|
||||
if(vars && group==s.solo_client_group) {
|
||||
const auto in_activity=read<std::uint8_t>(vars+0x104);
|
||||
const auto idle=read<std::uint32_t>(vars+0x108);
|
||||
if(in_activity || idle) {
|
||||
// ClientPlayerVars defaults confirmed by its native constructor
|
||||
// (5df4670). Offline AMP never supplies the final activity reset.
|
||||
const bool inactive=false;
|
||||
const std::uint32_t no_idle=0;
|
||||
SIZE_T written{};
|
||||
const bool reset_activity=WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(vars+0x104),&inactive,sizeof(inactive),&written) && written==sizeof(inactive);
|
||||
const bool reset_idle=WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(vars+0x108),&no_idle,sizeof(no_idle),&written) && written==sizeof(no_idle);
|
||||
logging::log(logging::Level::info,logging::Channel::progression,
|
||||
"Throwdown: confirmed solo exit for player {:#x}; client activity {} -> {}, idle {} -> {} (reset {}/{}).",
|
||||
ended,in_activity,read<std::uint8_t>(vars+0x104),idle,read<std::uint32_t>(vars+0x108),reset_activity,reset_idle);
|
||||
}
|
||||
} else logging::log(logging::Level::warning,logging::Channel::progression,
|
||||
"Throwdown: solo exit for {:#x} did not match the captured client activity; reset skipped.",ended);
|
||||
s.solo_client_activity=0;s.solo_client_player=0;s.solo_client_group=0;
|
||||
}
|
||||
|
||||
// While a plugin match blocks markers (kit::match().markers) the session marker conditions read
|
||||
// false and the pause menu routes to the activity, whose Quit the match's plugin hears.
|
||||
void complete_match_controls(Address vm) {
|
||||
if(!kit::match().markers || !vm || realm()!=client_realm)return;
|
||||
const auto resource=read<Address>(vm+0x38),instance=read<Address>(vm+0x30);
|
||||
if(!resource || !instance || read<Address>(instance)!=resource)return;
|
||||
const auto hash=read<std::uint32_t>(resource+0x10);
|
||||
const auto layout=read<std::array<std::uint32_t,10>>(resource+0x20);
|
||||
unsigned output_offset{};
|
||||
bool result=false,matched=false;
|
||||
if(hash==0x32c0b460 && layout==std::array<std::uint32_t,10>{64,32,10,52,4,0,0,3,0,0}) {
|
||||
output_offset=16;matched=true; // SessionMarkerReturn condition
|
||||
} else if((hash==0x870491b0 || hash==0xa5d0ee41) && layout==std::array<std::uint32_t,10>{48,64,68,95,3,0,0,8,720896,0}) {
|
||||
output_offset=8;matched=true; // Marker set/setting rumble conditions
|
||||
} else if(hash==0x3a5e5108 && layout==std::array<std::uint32_t,10>{64,8,37,113,4,0,0,9,786432,0}) {
|
||||
result=true;matched=true; // Native UI activity pause/marker routing
|
||||
} else if(hash==0x6998ec4b && layout==std::array<std::uint32_t,10>{48,288,98,211,3,0,3,327693,1572864,16777472}) {
|
||||
// QuitChallenge is the original pause modal's confirmed Quit Match
|
||||
// action. Its native picker may have retired already; still end the match.
|
||||
native_flag::quit_match();
|
||||
}
|
||||
if(!matched)return;
|
||||
const auto arguments=read<Address>(instance+((layout[0]+15U)&~15U)+8);
|
||||
const auto output=arguments?read<Address>(arguments+output_offset):0;
|
||||
SIZE_T written{};
|
||||
if(output)WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(output),&result,sizeof(result),&written);
|
||||
}
|
||||
|
||||
void complete_custom_details_presence(Address vm) {
|
||||
// This is the authored ActivitiesDetailsScreen -> MenusClosed condition,
|
||||
// not its OnEnd action. Let the native condition execute all its reads and
|
||||
// preserve its selected-menu/navigation/layer close reasons (local 0x80).
|
||||
// Its other term is !activity_present, local 0x81. Native presence means
|
||||
// DynamicPhaseHandler.PhaseSetEntities has entries when the dynamic manager
|
||||
// exists; otherwise it uses RuleActionManager.ActiveRuleActionHandles.
|
||||
// A custom match supplies that activity ownership only for its own Start
|
||||
// request. No fake native event, phase entity, camera or Page is created.
|
||||
auto& s=state();
|
||||
const auto selection=s.menu_trace_address.load(std::memory_order_acquire);
|
||||
if(!selection || s.menu_trace_thread.load(std::memory_order_relaxed)!=GetCurrentThreadId() || !vm)return;
|
||||
const auto resource=read<Address>(vm+0x38),instance=read<Address>(vm+0x30);
|
||||
constexpr std::array<std::uint32_t,10> layout{48,296,140,332,3,0,3,655385,2293760,0};
|
||||
if(!resource || !instance || read<Address>(instance)!=resource ||
|
||||
read<std::uint32_t>(resource+0x10)!=0x05edc9f8 ||
|
||||
read<std::array<std::uint32_t,10>>(resource+0x20)!=layout)return;
|
||||
if(!active() || realm()!=client_realm || !kit::match().in_activity)return;
|
||||
const auto pages=instance+48;
|
||||
const auto constants=read<Address>(pages),arguments=read<Address>(pages+8),locals=read<Address>(pages+16);
|
||||
if(!constants || !arguments || !locals ||
|
||||
read<std::uint32_t>(constants+8)!=0xaf4c67d7 ||
|
||||
read<std::uint32_t>(constants+0x28)!=0xcc4776b5 ||
|
||||
read<std::uint32_t>(constants+0x48)!=0xd71ee0a1)return;
|
||||
const auto output=read<Address>(arguments);
|
||||
if(!output || read<std::int32_t>(selection)!=1)return;
|
||||
std::array<std::uint8_t,140> values{};
|
||||
if(!memory::peek_bytes(locals,values.data(),values.size()))return;
|
||||
const auto original=read<std::uint8_t>(output);
|
||||
if(!s.menu_details_check_reported.exchange(true,std::memory_order_relaxed))
|
||||
logging::log(logging::Level::info,logging::Channel::ui,
|
||||
"PanelTrace native Details exit predicate: graph={:#x}, selected={}, native_exit={}, close_requested={}, native_activity_present={}, frame_result={}, output={:#x}, locals={:#x}.",
|
||||
0x05edc9f8,values[0x64],original,values[0x80],values[0x81],values[0x8b],output,locals);
|
||||
// The exact final native copy must agree with its saved result. Reject an
|
||||
// expired/nested frame or changed graph instead of borrowing its pointers.
|
||||
if(values[0x8b]!=original || values[0x89]!=1 || values[0x8a]!=1 ||
|
||||
!custom_details_presence_needed(true,values[0x64],original,values[0x80],values[0x81]))return;
|
||||
{
|
||||
std::lock_guard presence_lock(s.custom_ui_presence_mutex);
|
||||
if(!s.custom_ui_presence.owned())return;
|
||||
}
|
||||
if(s.lifetime.active() || s.lifetime.has_activity())return;
|
||||
const std::uint8_t keep_native_details=0;
|
||||
SIZE_T written{};
|
||||
if(WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(output),&keep_native_details,
|
||||
sizeof(keep_native_details),&written) && written==sizeof(keep_native_details) &&
|
||||
!s.menu_details_presence_reported.exchange(true,std::memory_order_relaxed))
|
||||
logging::log(logging::Level::info,logging::Channel::ui,
|
||||
"Throwdowns: native Details recognized custom activity owner; graph={:#x}, close_requested={}, native_activity_present={}, native_exit={} -> 0; native Back/camera/input unchanged.",
|
||||
0x05edc9f8,values[0x80],values[0x81],original);
|
||||
}
|
||||
|
||||
std::uint64_t player_in_activity_hook(std::uint32_t uid) {
|
||||
const auto original=state().player_in_activity(uid);
|
||||
if(native_activity_flag(original) || !kit::match().in_activity)return original;
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
// The original pause/marker UI must treat the local match participant
|
||||
// as playing an activity even after its temporary flag queue retires.
|
||||
// The query adapter remains independent of the scoped client UI lease.
|
||||
if(active() && realm()==client_realm) {
|
||||
const auto ids=sole_offline_player_ids();
|
||||
if(ids.matches_menu_player(uid) && client_activity_variables(ids.participant))return 1;
|
||||
}
|
||||
} catch(...) {}
|
||||
return original;
|
||||
}
|
||||
|
||||
// The throwdowner flow leaves its mode-select state in the same frame unless
|
||||
@@ -181,17 +382,19 @@ void send_event_hook(Address signature, Address output, const Address* arguments
|
||||
}
|
||||
const auto name = throwdown_event(hash);
|
||||
const auto current_realm = realm();
|
||||
if (current_realm == client_realm) prepare_native_solo_throwdown_leave(hash);
|
||||
if (current_realm == client_realm) observe_throwdown_send(hash, read<Address>(arguments[0]), arguments[1]);
|
||||
if (current_realm == client_realm && consume_custom_throwdown_send(hash, read<Address>(arguments[0]), arguments[1])) return;
|
||||
// CreateThrowdownQueue: the local player now hosts a throwdown
|
||||
// (a plugin mode's flag is not one).
|
||||
if (hash == 0x7fac81bb && !custom_throwdowns::registration_owned() && !state().throwdown_active.exchange(true))
|
||||
// Keep the native registration and waiting HUD. Only its Start
|
||||
// action is diverted to the plugin mode; ordinary Throwdowns are unchanged.
|
||||
if(current_realm==client_realm && consume_flag_throwdown_send(hash,read<Address>(arguments[0]),arguments[1])) return;
|
||||
// CreateThrowdownQueue: the local player now hosts a throwdown.
|
||||
if (current_realm == client_realm && hash == 0x7fac81bb && state().lifetime.create(sole_offline_player_id()))
|
||||
logging::log(logging::Level::info, logging::Channel::progression, "Throwdown: local player is hosting one.");
|
||||
// AddParticipantToCommunityEvent (the Join button): in one someone else placed,
|
||||
// which the Quit action must also leave. RemoveParticipant: left its queue.
|
||||
if (current_realm == client_realm && hash == 0xdc10392b && !state().throwdown_joined.exchange(true))
|
||||
if (current_realm == client_realm && hash == 0xdc10392b && state().lifetime.join(sole_offline_player_id()))
|
||||
logging::log(logging::Level::info, logging::Channel::progression, "Throwdown: local player joined one.");
|
||||
if (current_realm == client_realm && hash == 0xaa3f9957) state().throwdown_joined.store(false);
|
||||
if (current_realm == client_realm && hash == 0xaa3f9957) state().lifetime.leave_queue();
|
||||
loopback = !name.empty() && empty_target && current_realm == server_realm && sole_offline_player_id();
|
||||
static std::atomic<unsigned> logged{};
|
||||
if (loopback && logged.fetch_add(1) < 16)
|
||||
@@ -266,8 +469,7 @@ void debug_variable(const char* const* name, const std::uint32_t* fallback, std:
|
||||
std::array<char, wanted.size() + 1> value{};
|
||||
if (!memory::peek(text, value) || value[wanted.size()] != 0 ||
|
||||
std::string_view(value.data(), wanted.size()) != wanted) return;
|
||||
// A plugin mode's flag queue must never count down and start alone.
|
||||
*out = custom_throwdowns::registration_owned() ? 0U : 1U;
|
||||
*out = native_flag::registration_owned() ? 0U : 1U;
|
||||
static std::atomic<bool> logged{};
|
||||
if (!logged.exchange(true))
|
||||
logging::write(logging::Level::info, logging::Channel::progression,
|
||||
@@ -359,15 +561,15 @@ namespace {
|
||||
struct ModeParameter { const char* mode; const char* parameter; float minimum, maximum, fallback; };
|
||||
constexpr std::array mode_parameters{
|
||||
ModeParameter{"JamSession", "AllowOffBoard", 0, 1, 1}, ModeParameter{"JamSession", "AllowSessionMarkers", 0, 1, 1},
|
||||
ModeParameter{"JamSession", "MaxPlayers", 2, 10, 4}, ModeParameter{"JamSession", "Privacy", 0, 1, 0},
|
||||
ModeParameter{"JamSession", "MaxPlayers", 1, 10, 4}, ModeParameter{"JamSession", "Privacy", 0, 1, 0},
|
||||
ModeParameter{"JamSession", "ScoreFlips", 0, 1, 1}, ModeParameter{"JamSession", "ScoreGrabs", 0, 1, 1},
|
||||
ModeParameter{"JamSession", "ScoreGrinds", 0, 1, 1}, ModeParameter{"JamSession", "ScoreGroundTricks", 0, 1, 1},
|
||||
ModeParameter{"JamSession", "ScoreSlams", 0, 1, 0}, ModeParameter{"JamSession", "Timer", 60, 300, 180},
|
||||
ModeParameter{"JamSession", "WinCondition", 0, 3, 0}, ModeParameter{"JamSession", "Invite", 0, 2, 0},
|
||||
ModeParameter{"SpotBattle", "Rounds", 1, 5, 3}, ModeParameter{"SpotBattle", "TurnDuration", 10, 60, 30},
|
||||
ModeParameter{"SpotBattle", "Invite", 0, 2, 0}, ModeParameter{"SpotBattle", "MaxPlayers", 2, 10, 4},
|
||||
ModeParameter{"SpotBattle", "Invite", 0, 2, 0}, ModeParameter{"SpotBattle", "MaxPlayers", 1, 10, 4},
|
||||
ModeParameter{"SpotBattle", "Privacy", 0, 1, 0}, ModeParameter{"ThrowdownSkate", "TurnDuration", 10, 60, 30},
|
||||
ModeParameter{"ThrowdownSkate", "Invite", 0, 2, 0}, ModeParameter{"ThrowdownSkate", "MaxPlayers", 2, 10, 4},
|
||||
ModeParameter{"ThrowdownSkate", "Invite", 0, 2, 0}, ModeParameter{"ThrowdownSkate", "MaxPlayers", 1, 10, 4},
|
||||
ModeParameter{"ThrowdownSkate", "Privacy", 0, 1, 0}};
|
||||
|
||||
// Builds the array exactly as the native response flattener
|
||||
@@ -408,46 +610,241 @@ std::uint32_t local_native_player_id() noexcept {
|
||||
profile_runtime::PreserveError preserve;
|
||||
try { return active() ? sole_offline_player_id() : 0; } catch (...) { return 0; }
|
||||
}
|
||||
bool consume_match_marker_expression(Address vm,std::uint32_t pc) noexcept {
|
||||
if(pc || !vm || !kit::match().markers)return false;
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
if(!active() || realm()!=client_realm)return false;
|
||||
const auto resource=read<Address>(vm+0x38),instance=read<Address>(vm+0x30);
|
||||
return resource && instance && read<Address>(instance)==resource &&
|
||||
session_marker_action(read<std::uint32_t>(resource+0x10),
|
||||
read<std::array<std::uint32_t,10>>(resource+0x20),pc);
|
||||
} catch(...) {return false;}
|
||||
}
|
||||
|
||||
bool local_throwdown_active() noexcept {
|
||||
return state().throwdown_active.load(std::memory_order_acquire) || state().throwdown_joined.load(std::memory_order_acquire);
|
||||
return state().lifetime.active();
|
||||
}
|
||||
bool local_throwdown_event_active() noexcept {
|
||||
try {return state().lifetime.has_activity();} catch(...) {return true;}
|
||||
}
|
||||
void set_custom_activity_menu_request(Address address) noexcept {
|
||||
auto& s=state();
|
||||
s.menu_trace_address.store(0,std::memory_order_release);
|
||||
s.menu_details_presence_reported.store(false,std::memory_order_relaxed);
|
||||
s.menu_details_check_reported.store(false,std::memory_order_relaxed);
|
||||
std::int32_t value{};
|
||||
if(!address || !memory::peek(address,value))return;
|
||||
s.menu_trace_selection.store(value,std::memory_order_relaxed);
|
||||
s.menu_trace_thread.store(GetCurrentThreadId(),std::memory_order_relaxed);
|
||||
s.menu_trace_address.store(address,std::memory_order_release);
|
||||
}
|
||||
void sync_custom_activity_ui_presence(bool participant) noexcept {
|
||||
profile_runtime::PreserveError preserve;
|
||||
auto& s=state();
|
||||
try {
|
||||
if(!active())return;
|
||||
std::lock_guard presence_lock(s.custom_ui_presence_mutex);
|
||||
if(!participant && !s.custom_ui_presence.owned() && s.custom_ui_diagnostic.empty())return;
|
||||
const auto caller_realm=realm();
|
||||
const auto uid=sole_offline_player_id();
|
||||
std::uint32_t menu_player{};
|
||||
const auto vars=uid?client_activity_variables(uid,&menu_player):0;
|
||||
const auto group=vars?read<std::uint32_t>(vars+0x100):0;
|
||||
const auto current=vars?read<std::uint8_t>(vars+0x104)!=0:false;
|
||||
const bool native_flag=vars && s.player_in_activity && native_activity_flag(s.player_in_activity(menu_player));
|
||||
const bool queued=s.lifetime.active(),native_activity=s.lifetime.has_activity();
|
||||
const bool was_owned=s.custom_ui_presence.owned();
|
||||
const auto diagnostic=[&](const char* result) {
|
||||
if(!participant && !was_owned && s.custom_ui_diagnostic.empty())return;
|
||||
const auto message=std::format(
|
||||
"wanted={}, caller_realm={:#x}, participant={}, menu_player={}, variables={:#x}, group={}, flag={}, getter={}, idle={}, queue={}, native_activity={}, owned={}, result={}",
|
||||
participant,caller_realm,uid,menu_player,vars,group,current,native_flag,
|
||||
vars?read<std::uint32_t>(vars+0x108):0,queued,native_activity,s.custom_ui_presence.owned(),result);
|
||||
const auto now=GetTickCount64();
|
||||
if(message!=s.custom_ui_diagnostic || ((participant || was_owned) && now>=s.custom_ui_diagnostic_at+1000)) {
|
||||
logging::log(logging::Level::info,logging::Channel::ui,"Throwdowns: activity UI handoff: {}.",message);
|
||||
s.custom_ui_diagnostic=message;s.custom_ui_diagnostic_at=now;
|
||||
}
|
||||
};
|
||||
// Verify the existing getter and reflected flag identify the same
|
||||
// player before acquiring. No idle/group/backend values are changed.
|
||||
if(participant && (!vars || !s.player_in_activity || native_flag!=current)) {
|
||||
diagnostic(!uid?"no sole native player":!vars?"no verified native client player":
|
||||
!s.player_in_activity?"native getter unavailable":"native getter disagrees with client player");
|
||||
return;
|
||||
}
|
||||
bool write_failed{};
|
||||
s.custom_ui_presence.update(participant && !queued,vars,uid,group,current,
|
||||
native_activity,[&](bool value) {
|
||||
SIZE_T written{};
|
||||
const std::uint8_t flag=value?1:0;
|
||||
const bool applied=WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(vars+0x104),
|
||||
&flag,sizeof(flag),&written) && written==sizeof(flag);
|
||||
if(applied && (native_activity_flag(s.player_in_activity(menu_player))!=value)) {
|
||||
// A same-id object from another context cannot be the
|
||||
// native UI owner. Undo this one-byte attempt immediately.
|
||||
const std::uint8_t original=current?1:0;
|
||||
WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(vars+0x104),&original,sizeof(original),&written);
|
||||
write_failed=true;
|
||||
return false;
|
||||
}
|
||||
if(applied)logging::log(logging::Level::info,logging::Channel::ui,
|
||||
"Throwdowns: client activity UI presence {} -> {} for player {}, group {}, variables {:#x}; native idle/backend unchanged.",
|
||||
current,value,uid,group,vars);
|
||||
if(!applied)write_failed=true;
|
||||
return applied;
|
||||
});
|
||||
diagnostic(write_failed?"write/getter verification failed":queued?"native queue owns activity":
|
||||
native_activity?"native event owns activity":s.custom_ui_presence.owned()?"scoped client flag owned":
|
||||
participant && current?"client flag already native":participant?"client flag not acquired":"released");
|
||||
if(!participant && !s.custom_ui_presence.owned())s.custom_ui_diagnostic.clear();
|
||||
} catch(const std::exception& error) {
|
||||
static std::uint64_t next_log{};const auto now=GetTickCount64();
|
||||
if(now>=next_log) {next_log=now+10000;logging::log(logging::Level::warning,logging::Channel::ui,
|
||||
"Throwdowns: client activity UI presence unavailable: {}",error.what());}
|
||||
} catch(...) {}
|
||||
}
|
||||
bool local_throwdown_host() noexcept { return state().lifetime.host(); }
|
||||
void native_throwdown_player_limit(std::uint32_t limit) noexcept {
|
||||
try { state().lifetime.player_limit(limit); } catch (...) {}
|
||||
}
|
||||
Address native_solo_throwdown_activity(std::uint32_t player) noexcept {
|
||||
try { return state().lifetime.solo_activity(player); } catch (...) { return 0; }
|
||||
}
|
||||
bool native_solo_throwdown_menu_cleanup_pending() noexcept {
|
||||
try { return !native_flag::registration_owned() && state().lifetime.menu_cleanup_pending(); } catch (...) { return false; }
|
||||
}
|
||||
void native_solo_throwdown_menu_cleaned() noexcept {
|
||||
try { state().lifetime.menu_cleaned(); } catch (...) {}
|
||||
}
|
||||
void release_native_flag_placeholder() noexcept {
|
||||
try { state().lifetime.clear(); } catch (...) {}
|
||||
}
|
||||
void native_throwdown_activity_started(Address activity, std::uint32_t player) noexcept {
|
||||
try {
|
||||
if (state().lifetime.activity_started(activity, player))
|
||||
logging::log(logging::Level::info, logging::Channel::progression,
|
||||
"Throwdown: local player {:#x} entered native activity {:#x}.", player, activity);
|
||||
} catch (...) {}
|
||||
}
|
||||
void native_throwdown_participant_left(Address activity, std::uint32_t player) noexcept {
|
||||
try {
|
||||
if (state().lifetime.participant_left(activity, player))
|
||||
logging::log(logging::Level::info, logging::Channel::progression,
|
||||
"Throwdown: ended after player {:#x} left native activity {:#x}; ownership cleared.", player, activity);
|
||||
} catch (...) {}
|
||||
}
|
||||
void native_throwdown_level_left() noexcept {
|
||||
retire_native_queue_hud();
|
||||
set_custom_activity_menu_request(0);
|
||||
sync_custom_activity_ui_presence(false);
|
||||
try {
|
||||
state().solo_client_activity=0;state().solo_client_player=0;state().solo_client_group=0;
|
||||
if (state().lifetime.clear())
|
||||
logging::write(logging::Level::info, logging::Channel::progression,
|
||||
"Throwdown: ownership cleared before leaving the level.");
|
||||
} catch (...) {}
|
||||
}
|
||||
bool local_throwdown_host() noexcept { return state().throwdown_active.load(std::memory_order_acquire); }
|
||||
|
||||
void complete_native_throwdown_parameters(Address vm) noexcept {
|
||||
pump_throwdown_lab(vm);
|
||||
if (custom_throwdowns::registration_owned()) {
|
||||
// Native expressions can select, then immediately retire, the menu in a
|
||||
// single frame. Sample after the deepest completed call on its UI thread
|
||||
// so the log identifies the writer instead of only the next 100ms snapshot.
|
||||
{
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
const auto resource = vm ? read<Address>(vm + 0x38) : 0;
|
||||
const auto instance = vm ? read<Address>(vm + 0x30) : 0;
|
||||
const auto hash = resource ? read<std::uint32_t>(resource + 0x10) : 0;
|
||||
if (resource && realm() == client_realm && custom_throwdowns::placement_active())
|
||||
custom_throwdowns::observe_graph(hash);
|
||||
// Authored predicate outputs (page 1 of the graph's registers): the
|
||||
// waiting queue normally locks the Toolbox, and Start waits for a
|
||||
// full queue. A plugin mode's flag is neither: its host starts it.
|
||||
constexpr std::array<std::uint32_t, 10> disabled_layout{64, 104, 44, 177, 3, 0, 2, 14, 720896, 66560};
|
||||
constexpr std::array<std::uint32_t, 10> start_layout{144, 1360, 1565, 1445, 18, 0, 7, 2293833, 5832773, 17434624};
|
||||
if (instance && read<Address>(instance) == resource && realm() == client_realm &&
|
||||
((hash == 0xc3999b06 && read<std::array<std::uint32_t, 10>>(resource + 0x20) == disabled_layout) ||
|
||||
(hash == 0x1a0c8c31 && read<std::array<std::uint32_t, 10>>(resource + 0x20) == start_layout))) {
|
||||
const auto frame = read<std::uint32_t>(resource + 0x20);
|
||||
const auto arguments = read<Address>(instance + ((frame + 15U) & ~15U) + 8);
|
||||
if (arguments) {
|
||||
const bool result = hash == 0x1a0c8c31 && custom_throwdowns::flag_down();
|
||||
// Both return through opcode 0x32's borrowed output pointer.
|
||||
const auto output = read<Address>(arguments + (hash == 0xc3999b06 ? 8 : 96));
|
||||
SIZE_T written{};
|
||||
const bool wrote = output && WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(output), &result,
|
||||
sizeof(result), &written) && written == sizeof(result);
|
||||
static std::atomic<unsigned> reports{};
|
||||
if (hash == 0x1a0c8c31 && reports.fetch_add(1) < 40)
|
||||
logging::log(logging::Level::info, logging::Channel::progression,
|
||||
"Custom throwdown: waiting card Start {} (written {}).", result ? "offered" : "held back", wrote);
|
||||
auto& s=state();const auto address=s.menu_trace_address.load(std::memory_order_acquire);
|
||||
std::int32_t value{};
|
||||
if(address && s.menu_trace_thread.load(std::memory_order_relaxed)==GetCurrentThreadId() &&
|
||||
memory::peek(address,value)) {
|
||||
const auto previous=s.menu_trace_selection.exchange(value,std::memory_order_relaxed);
|
||||
if(previous!=value) {
|
||||
const auto resource=vm?read<Address>(vm+0x38):0;
|
||||
logging::log(logging::Level::info,logging::Channel::ui,
|
||||
"PanelTrace selection changed {} -> {} after native graph {:#x}, resource={:#x}, VM={:#x}, field={:#x}, realm={:#x}.",
|
||||
previous,value,resource?read<std::uint32_t>(resource+0x10):0,resource,vm,address,realm());
|
||||
}
|
||||
}
|
||||
} catch (...) {}
|
||||
} catch(...) {}
|
||||
}
|
||||
{
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
if(active() && realm()==client_realm)complete_solo_client_exit();
|
||||
complete_match_controls(vm);
|
||||
complete_custom_details_presence(vm);
|
||||
const auto join_resource=vm?read<Address>(vm+0x38):0;
|
||||
if(join_resource && read<std::uint32_t>(join_resource+0x10)==0xebb4ab69 && active() && realm()==client_realm)
|
||||
observe_native_custom_join_details(vm);
|
||||
// Replace presentation at the native publication boundary, rather
|
||||
// than waiting for the next throttled UI/camera confirmation tick.
|
||||
// Real activities, other realms/threads and unarmed requests keep
|
||||
// their own contents. The native outer Page is never replaced.
|
||||
auto& s=state();const auto selected=s.menu_trace_address.load(std::memory_order_acquire);
|
||||
if(selected && s.menu_trace_thread.load(std::memory_order_relaxed)==GetCurrentThreadId()) {
|
||||
const auto resource=vm?read<Address>(vm+0x38):0,instance=vm?read<Address>(vm+0x30):0;
|
||||
if(resource && instance && read<Address>(instance)==resource &&
|
||||
menu_data::native_activity_page_publish_graph(read<std::uint32_t>(resource+0x10),
|
||||
read<std::array<std::uint32_t,10>>(resource+0x20)) &&
|
||||
active() && realm()==client_realm && read<std::int32_t>(selected)==1)
|
||||
prepare_native_throwdown_match_panel();
|
||||
}
|
||||
} catch(...) {}
|
||||
}
|
||||
try {
|
||||
const auto resource=vm?read<Address>(vm+0x38):0;
|
||||
if(resource && read<std::uint32_t>(resource+0x10)==0x627382a6 && active() && realm()==client_realm)
|
||||
observe_native_flag_interaction(vm);
|
||||
} catch(...) {}
|
||||
const bool flag_queue=native_flag::registration_owned();
|
||||
const bool solo_queue=!flag_queue && native_solo_throwdown_waiting();
|
||||
if(flag_queue || solo_queue) {
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
const auto resource=vm?read<Address>(vm+0x38):0;
|
||||
const auto instance=vm?read<Address>(vm+0x30):0;
|
||||
const auto hash=resource?read<std::uint32_t>(resource+0x10):0;
|
||||
// Authored predicate outputs (EBX GraphPortRegister page 1): the
|
||||
// retail queue normally locks the Toolbox while waiting. A plugin
|
||||
// mode's registration may reopen setup, and may start when its
|
||||
// waiting card says so (can_start). A stock one-player host queue
|
||||
// may start alone; other queues retain their authored predicates.
|
||||
constexpr std::array<std::uint32_t,10> disabled_layout{64,104,44,177,3,0,2,14,720896,66560};
|
||||
constexpr std::array<std::uint32_t,10> start_layout{144,1360,1565,1445,18,0,7,2293833,5832773,17434624};
|
||||
if(instance && read<Address>(instance)==resource && realm()==client_realm &&
|
||||
((flag_queue && hash==0xc3999b06 && read<std::array<std::uint32_t,10>>(resource+0x20)==disabled_layout) ||
|
||||
(hash==0x1a0c8c31 && read<std::array<std::uint32_t,10>>(resource+0x20)==start_layout))) {
|
||||
const auto frame=read<std::uint32_t>(resource+0x20);
|
||||
const auto arguments=read<Address>(instance+((frame+15U)&~15U)+8);
|
||||
if(arguments) {
|
||||
bool result=false;
|
||||
if(hash==0x1a0c8c31) {
|
||||
if(solo_queue)result=true;
|
||||
else if(native_flag::registration_waiting()) {
|
||||
const auto docs=kit::documents_for(native_flag::mode());
|
||||
result=docs && docs->waiting && docs->waiting->can_start;
|
||||
}
|
||||
}
|
||||
// Both graphs return through opcode 0x32's borrowed
|
||||
// output pointer on page 1, not their local scratch page.
|
||||
const auto output=read<Address>(arguments+(hash==0xc3999b06?8:96));
|
||||
SIZE_T written{};
|
||||
if(output) WriteProcessMemory(GetCurrentProcess(),reinterpret_cast<void*>(output),&result,sizeof(result),&written);
|
||||
}
|
||||
}
|
||||
} catch(...) {}
|
||||
}
|
||||
if(native_flag::placement_active()) {
|
||||
profile_runtime::PreserveError preserve;
|
||||
try {
|
||||
const auto resource=vm?read<Address>(vm+0x38):0;
|
||||
if(resource && realm()==client_realm) {
|
||||
const auto hash=read<std::uint32_t>(resource+0x10);
|
||||
native_flag::observe_graph(hash);
|
||||
}
|
||||
} catch(...) {}
|
||||
}
|
||||
// The throwdown is over once a client throwdown enters its end state or
|
||||
// leaves in progress. (Starting the round destroys the registration point
|
||||
@@ -459,8 +856,7 @@ void complete_native_throwdown_parameters(Address vm) noexcept {
|
||||
const auto resource = vm ? read<Address>(vm + 0x38) : 0;
|
||||
switch (resource ? read<std::uint32_t>(resource + 0x10) : 0) {
|
||||
case 0x224de9ab: case 0xd373badf: case 0x2ea80cac: case 0x1d82a056: // Client*Throwdown.EnterEnd
|
||||
case 0x91cd7c71: // ClientThrowdownBase.EnterLeaveInProgress
|
||||
if (state().throwdown_active.exchange(false) | state().throwdown_joined.exchange(false))
|
||||
if (realm() == client_realm && state().lifetime.ended())
|
||||
logging::log(logging::Level::info, logging::Channel::progression,
|
||||
"Throwdown: ended (graph {:#x}).", read<std::uint32_t>(resource + 0x10));
|
||||
break;
|
||||
@@ -636,6 +1032,7 @@ void initialize_native_throwdowns(Address base) noexcept {
|
||||
install_parse_int(base);
|
||||
struct Hook { Address rva; std::array<unsigned char, 19> prefix; void* replacement; void** original; };
|
||||
const std::array hooks{
|
||||
Hook{throwdowns::player_in_activity,throwdowns::player_in_activity_prefix,reinterpret_cast<void*>(&player_in_activity_hook),reinterpret_cast<void**>(&s.player_in_activity)},
|
||||
Hook{throwdowns::mode_available, throwdowns::mode_available_prefix, reinterpret_cast<void*>(&mode_available_hook), reinterpret_cast<void**>(&s.mode_available)},
|
||||
Hook{throwdowns::player_info, throwdowns::player_info_prefix, reinterpret_cast<void*>(&player_info_hook), reinterpret_cast<void**>(&s.player_info)},
|
||||
Hook{throwdowns::service_player_info, throwdowns::service_player_info_prefix, reinterpret_cast<void*>(&service_player_info_hook), reinterpret_cast<void**>(&s.service_player_info)},
|
||||
|
||||
Reference in new issue
Block a user