Files
ReSkate/Extension/Progression/local_entitlement_trigger_runtime.cpp
T
2026-10-02 14:40:41 -05:00

127 lines
5.8 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "Engine/Core/Profiling/profiler.h"
#include "Extension/Customization/local_customization_runtime.h"
#include "Extension/Profile/runtime_internal.h"
#include "local_challenge_runtime.h"
#include "local_entitlement_trigger_runtime.h"
#include "Extension/Multiplayer/Hud/native_party.h"
#include "Extension/Throwdowns/native_throwdowns.h"
#include <algorithm>
namespace dingosdk::profile_runtime {
// EntitlementTrigger's two retail graphs compute:
// connected && session_ready && (HasEntitlement(id) == RequireEntitlement).
// Returning ownership alone leaves both the interaction and map branches idle
// offline. Supply *provider readiness* only while those exact graphs execute;
// their existing entitlement lookup still decides ownership from the local profile.
// No connection state, ECS activation bits, or map entries are fabricated.
// See analysis/local-entitlement-trigger-readiness.md for the bytecode contract.
thread_local std::uintptr_t executing_expression{};
thread_local const ExpressionExecutionScope* expression_scope{};
void execute_expression_hook(std::uintptr_t vm, std::uint32_t pc) {
// The setup helper only prepares registers. Native calls actually execute
// in 1416d0b90 / 1416d1a10, including direct ECS interpreter entry paths.
// Keep the exact cursor and profiler context when entering either runner.
ExpressionExecutionScope scope(vm);
{
DINGO_PROFILE_ZONE("hooks/expression start");
initialize_starter_from_expression(vm, pc);
}
multiplayer::execute_native_party_expression(vm, pc, 0,
[](std::uintptr_t expression, std::uint32_t cursor, std::uintptr_t) {
local_runtime().execute_expression(expression, cursor);
});
DINGO_PROFILE_ZONE("hooks/expression end");
deliver_local_challenge_completion(vm, pc);
multiplayer::complete_native_throwdown_parameters(vm);
}
void execute_profiled_expression_hook(std::uintptr_t vm, std::uint32_t pc, std::uintptr_t profiler) {
ExpressionExecutionScope scope(vm);
{
DINGO_PROFILE_ZONE("hooks/expression start");
initialize_starter_from_expression(vm, pc);
}
multiplayer::execute_native_party_expression(vm, pc, profiler, local_runtime().execute_profiled_expression);
DINGO_PROFILE_ZONE("hooks/expression end");
deliver_local_challenge_completion(vm, pc);
multiplayer::complete_native_throwdown_parameters(vm);
}
unsigned local_entitlement_trigger_graph() {
if (!executing_expression || !local_runtime().active.load(std::memory_order_acquire)) return 0;
std::uintptr_t instance{}, resource{}, current_resource{};
std::uint32_t key{};
// Asked by connection/session predicates in many scripts: the running graph's key is peeked
// first (no system call), and the rest read only for one of the two graphs.
if (!memory::peek(executing_expression + 0x38, current_resource) || !memory::peek(current_resource + 0x10, key) ||
std::ranges::none_of(entitlement_trigger_graphs, [&](const auto& graph) { return graph.key == key; }) ||
!read(executing_expression + 0x30, instance) || !read(instance, resource) || resource != current_resource) return 0;
for (unsigned i = 0; i < entitlement_trigger_graphs.size(); ++i) {
const auto& graph = entitlement_trigger_graphs[i];
if (key != graph.key) continue;
std::array<std::uint32_t, 10> layout{};
if (read(resource + 0x20, layout) && layout == graph.layout) return i + 1;
}
return 0;
}
void append_entitlement_trigger_context(std::ostringstream& event) {
// Called only for the first saved lookup of each bus-stop ID (bounded by
// logged_entitlements). Capture the actual executing graph for live QA,
// including a rejected layout, without recording entity/player data.
std::uintptr_t resource{}, instance{}, instance_resource{};
std::uint32_t key{};
std::array<std::uint32_t, 10> layout{};
event << ",\"script\":";
if (!executing_expression || !read(executing_expression + 0x38, resource) ||
!read(resource + 0x10, key) || !read(resource + 0x20, layout)) {
event << "null";
return;
}
const bool instance_matches = read(executing_expression + 0x30, instance) &&
read(instance, instance_resource) && instance_resource == resource;
event << "{\"key\":" << key << ",\"instance_matches\":" << (instance_matches ? "true" : "false")
<< ",\"recognized\":" << local_entitlement_trigger_graph() << ",\"layout\":[";
for (std::size_t i = 0; i < layout.size(); ++i) event << (i ? "," : "") << layout[i];
event << "]}";
}
bool local_trigger_ready(unsigned predicate) {
// Preserve the native caller's LastError even when safe-read validation
// rejects the graph and the caller subsequently falls back to the engine.
PreserveError preserve;
const auto graph = local_entitlement_trigger_graph();
if (!graph) return false;
const unsigned bit = 1u << ((graph - 1) * 2 + predicate);
if (!(local_runtime().logged_trigger_ready.fetch_or(bit) & bit)) {
try {
std::ostringstream event;
event << "{\"event\":\"local_profile_trigger_ready\",\"graph\":"
<< std::quoted(graph == 1 ? "EntitlementTrigger.Activate" : "EntitlementTrigger.OnEntitlementsChanged")
<< ",\"gate\":" << std::quoted(predicate ? "session" : "connection")
<< ",\"source\":\"saved_profile\"}";
dingosdk::logging::event(dingosdk::logging::Channel::progression, event.str().c_str());
} catch (...) {}
}
return true;
}
bool trigger_online_hook() {
return local_trigger_ready(0) || multiplayer::native_throwdown_ready(0) || local_runtime().trigger_online();
}
bool trigger_session_hook() {
return local_trigger_ready(1) || multiplayer::native_throwdown_ready(1) || local_runtime().trigger_session();
}
}