mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-27 11:57:08 +01:00
Plugins: reskate.tricks.label and record_as, and Rock to Fakie as a mod on them
Two general trick APIs on the hooks TheRealEJR found for his Rock to Fakie mod (pull request 285):
- tricks.label(from, to) ("hud"): the trick ticker shows `to` where the game's label is exactly
`from`, on the ticker's own call route and in its own buffer only, when it has room; 16 labels a
plugin, given back when it stops.
- tricks.record_as(from, to, within_ms) ("skater.control"): the next trick the recorder gets whose
id is one of `from`, within the window, is recorded as `to`, through a changed copy of the packet.
One ask a plugin, used once; refused while a throwdown runs, as trick replacements are.
The hooks pass everything on unchanged while no plugin asks.
contrib/plugins/rock-to-fakie is his feature in Lua: while the physics state holds a stall (400 or
401) the ticker shows Rock to Fakie for a Board Stall or a Lip Stall, and B pressed in it asks that
the stall be recorded as the game's fakie_rock (187). Off until `rocktofakie on`. Checked in the
plugin tests, with the new functions' permissions.
This commit is contained in:
1 parent
e6de1dfd64
commit
cf43231b3e
13 files changed
+515
-1
No files matched your search
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace dingosdk::game::build::v20260929::trick_rewrite {
|
||||
// Supported SHA-256 fbce74d5e28ef525dbba2cb4adbebc13405bdbd88f31bc940bca45e4ae88b8f9.
|
||||
// Found by TheRealEJR for the Rock to Fakie mod (TheRealEJR/ReSkate-Rock-to-Fakie-mod v1.4.15 to
|
||||
// v1.4.42, pull request 285), with a read-only recorder of the local skater's physics states, the
|
||||
// trick recorder's packets and the localized labels the trick ticker asks for.
|
||||
|
||||
// void record(recorder, const packet*): the trick recorder. It copies the 104-byte packet into its
|
||||
// history before it returns. Packet +0x0c is the trick id (at most 0x150), +0x1c nonzero when valid.
|
||||
// In the same catalogue a Board Stall records id 9 (bs_board), a Lip Stall id 12 (bs_lip), and 187 is
|
||||
// the game's own fakie_rock (the internal-name lookup at 0x4783030 resolves all three).
|
||||
inline constexpr std::uintptr_t record_trick = 0x47803a0;
|
||||
inline constexpr std::array<unsigned char, 32> record_trick_prefix{
|
||||
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x80,0x7a,0x1c,0x00,0x48,0x8b,
|
||||
0xda,0x48,0x8b,0xf9,0x74,0x75,0x81,0x7a,0x0c,0x50,0x01,0x00,0x00,0x77,0x6c,0x48};
|
||||
inline constexpr std::size_t trick_packet_size = 0x68;
|
||||
inline constexpr std::uintptr_t trick_packet_id = 0x0c, trick_packet_valid = 0x1c;
|
||||
inline constexpr std::uint32_t trick_id_max = 0x150;
|
||||
|
||||
// bool resolve(object, const char* key, char* buffer, uint32 capacity): a virtual method (found
|
||||
// only in a vtable, no direct calls) that writes a key's localized text into the caller's buffer;
|
||||
// AL is true on success. The trick ticker's labels come through it ("Board Stall", "Lip Stall"),
|
||||
// with these three return addresses as frames 2, 3 and 4 of the call's unwound stack (frame 0 the
|
||||
// hook, frame 1 its caller). No label but the ticker's was seen on that route.
|
||||
inline constexpr std::uintptr_t resolve_localized_key = 0x2e1ac10;
|
||||
inline constexpr std::array<unsigned char, 11> resolve_localized_key_prefix{
|
||||
0x40,0x53,0x55,0x56,0x57,0x41,0x56,0x48,0x83,0xec,0x20};
|
||||
inline constexpr std::array<std::uintptr_t, 3> ticker_label_route{0x758078, 0x59c1241, 0x16d2178};
|
||||
}
|
||||
@@ -85,6 +85,16 @@ struct FakeGame final : Game {
|
||||
if (words.empty()) return "game";
|
||||
return words.front() == "ringing" ? "1" : "";
|
||||
}
|
||||
std::string trick_label(std::string_view plugin, std::string_view from, std::string_view to) override {
|
||||
calls.push_back(std::format("trick label {} {}={}", plugin, from, to));
|
||||
return {};
|
||||
}
|
||||
std::string trick_record_as(std::string_view plugin, std::vector<std::uint32_t> from, std::uint32_t to, int within_ms) override {
|
||||
std::string ids;
|
||||
for (const auto id : from) ids += (ids.empty() ? "" : ",") + std::to_string(id);
|
||||
calls.push_back(std::format("trick record {} {}->{} {}", plugin, ids, to, within_ms));
|
||||
return {};
|
||||
}
|
||||
std::string board_toss(std::string_view plugin, std::vector<int> bones, const Vec3& velocity, const Vec3&, float ground) override {
|
||||
calls.push_back(std::format("board toss {} {} {:.0f} {:.0f}", plugin, bones.size(), velocity[1], ground));
|
||||
return {};
|
||||
@@ -530,6 +540,10 @@ void a_permission_must_be_declared() {
|
||||
assert(reskate.package.variants() == nil, "a mod without variants has none")
|
||||
ok, err = pcall(reskate.package.choose, "wide")
|
||||
assert(not ok and err:find("needs the \"variants\" permission"), err)
|
||||
ok, err = pcall(reskate.tricks.label, "Board Stall", "Rock to Fakie")
|
||||
assert(not ok and err:find("needs the \"hud\" permission"), err)
|
||||
ok, err = pcall(reskate.tricks.record_as, {9}, 187, 100)
|
||||
assert(not ok and err:find("needs the \"skater.control\" permission"), err)
|
||||
reskate.console.print("never")
|
||||
)")});
|
||||
stopped(host, "bare", "raised an error");
|
||||
@@ -1851,6 +1865,43 @@ void dual_stick_edits_the_companions_settings() {
|
||||
"Goofy picked on the page is written:\n" + after + environment.all());
|
||||
}
|
||||
|
||||
// Rock to Fakie as a mod (contrib/plugins/rock-to-fakie), on reskate.tricks.label and record_as: off
|
||||
// until switched on; then a held stall shows the label, B pressed in it asks for fakie_rock, and
|
||||
// leaving the stall gives the label back.
|
||||
void rock_to_fakie_relabels_and_records_the_stall() {
|
||||
TestEnvironment environment;
|
||||
std::filesystem::remove_all(scratch / "storage" / "rock-to-fakie");
|
||||
auto& game = environment.fake;
|
||||
game.telemetry = SkaterTelemetry{};
|
||||
game.telemetry->watch.valid = true;
|
||||
game.telemetry->watch.state = 400;
|
||||
game.pad_bits_now = 0u;
|
||||
Host host(environment, Side::client);
|
||||
host.apply({contributed("rock-to-fakie")});
|
||||
running(host, "rock-to-fakie", environment);
|
||||
ticks(host, environment, 5);
|
||||
check(!game.called("trick label"), "Off by default: nothing is asked\n" + environment.all());
|
||||
check(host.run_console("rocktofakie on") && environment.printed("rocktofakie: on."), "rocktofakie on");
|
||||
ticks(host, environment, 2);
|
||||
check(game.called("trick label rock-to-fakie Board Stall=Rock to Fakie") && game.called("trick label rock-to-fakie Lip Stall=Rock to Fakie"),
|
||||
"A held stall is labelled\n" + environment.all());
|
||||
game.pad_bits_now = 0x2000u;
|
||||
ticks(host, environment, 1);
|
||||
check(game.called("trick record rock-to-fakie 9,12->187 250"), "B in the stall asks for fakie_rock\n" + environment.all());
|
||||
game.calls.clear();
|
||||
ticks(host, environment, 3);
|
||||
check(!game.called("trick record"), "B held asks once\n" + environment.all());
|
||||
game.telemetry->watch.state = 604;
|
||||
game.pad_bits_now = 0u;
|
||||
ticks(host, environment, 1);
|
||||
check(game.called("trick label rock-to-fakie Board Stall=") && game.called("trick label rock-to-fakie Lip Stall="),
|
||||
"Leaving the stall gives the labels back\n" + environment.all());
|
||||
game.calls.clear();
|
||||
game.pad_bits_now = 0x2000u;
|
||||
ticks(host, environment, 1);
|
||||
check(!game.called("trick record"), "B outside a stall asks nothing\n" + environment.all());
|
||||
}
|
||||
|
||||
void board_break_snaps_the_board_on_a_slam() {
|
||||
TestEnvironment environment;
|
||||
std::filesystem::remove_all(scratch / "storage" / "board-break");
|
||||
@@ -2592,6 +2643,7 @@ int main() {
|
||||
reskate_test_passes_its_own_checks();
|
||||
road_rash_marks_the_skater_as_it_bails();
|
||||
dual_stick_edits_the_companions_settings();
|
||||
rock_to_fakie_relabels_and_records_the_stall();
|
||||
board_break_snaps_the_board_on_a_slam();
|
||||
fpv_drone_flies_and_follows();
|
||||
hall_of_meat_scores_a_bail();
|
||||
|
||||
@@ -274,6 +274,42 @@ int gesture(lua_State* L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// tricks.label(from, to): the trick ticker shows `to` where the game's label is exactly `from`
|
||||
// ("Board Stall"); to nil, that label is the game's again. 1 to 63 characters each, 16 labels a plugin.
|
||||
int label(lua_State* L) {
|
||||
auto& plugin = allowed(L, "hud", "reskate.tricks.label");
|
||||
std::size_t from_length = 0, to_length = 0;
|
||||
const char* from = luaL_checklstring(L, 1, &from_length);
|
||||
const char* to = lua_isnoneornil(L, 2) ? "" : luaL_checklstring(L, 2, &to_length);
|
||||
if (!from_length || from_length > 63 || to_length > 63) luaL_error(L, "a label is 1 to 63 characters");
|
||||
return result(L, game_of(plugin).trick_label(plugin.id(), std::string_view(from, from_length), std::string_view(to, to_length)));
|
||||
}
|
||||
// tricks.record_as(from, to, within_ms): the next trick the game records whose id is one of `from`
|
||||
// (a list of 1 to 16 ids, or one id), within `within_ms` (1 to 2000), is recorded as the id `to`.
|
||||
// The game's trick ids, 0 to 336. One pending record a plugin: a new one replaces it.
|
||||
int record_as(lua_State* L) {
|
||||
auto& plugin = allowed(L, "skater.control", "reskate.tricks.record_as");
|
||||
const auto id_at = [&](int index) {
|
||||
const double value = luaL_checknumber(L, index);
|
||||
if (!(value >= 0 && value <= 336) || value != std::floor(value)) luaL_error(L, "a trick id is a whole number, 0 to 336");
|
||||
return static_cast<std::uint32_t>(value);
|
||||
};
|
||||
std::vector<std::uint32_t> from;
|
||||
if (lua_istable(L, 1)) {
|
||||
const int count = lua_objlen(L, 1);
|
||||
if (count < 1 || count > 16) luaL_error(L, "a record names 1 to 16 trick ids");
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
lua_rawgeti(L, 1, i);
|
||||
from.push_back(id_at(lua_gettop(L)));
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else from.push_back(id_at(1));
|
||||
const auto to = id_at(2);
|
||||
const double within = luaL_checknumber(L, 3);
|
||||
if (!(within >= 1 && within <= 2000)) luaL_error(L, "a record waits 1 to 2000 ms");
|
||||
return result(L, game_of(plugin).trick_record_as(plugin.id(), std::move(from), to, static_cast<int>(within)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void install_services_api(Plugin&, lua_State* L) {
|
||||
@@ -282,6 +318,6 @@ void install_services_api(Plugin&, lua_State* L) {
|
||||
add_functions(L, "session", {{"spectate", spectate}, {"world", world}});
|
||||
add_functions(L, "camera", {{"free", free_camera}, {"pad_flight", pad_flight}});
|
||||
add_functions(L, "physics", {{"competition", competition}, {"stock_ready", stock_ready}, {"prepare", prepare}});
|
||||
add_functions(L, "tricks", {{"feed", trick_feed}, {"gesture", gesture}});
|
||||
add_functions(L, "tricks", {{"feed", trick_feed}, {"gesture", gesture}, {"label", label}, {"record_as", record_as}});
|
||||
}
|
||||
} // namespace dingosdk::plugins
|
||||
@@ -362,6 +362,13 @@ public:
|
||||
// The flick diagram of a flip trick as skate. names it (trick_gestures.h): read in the
|
||||
// background the first time; empty until then, or for a trick without one.
|
||||
virtual std::vector<std::array<float, 2>> trick_gesture(std::string_view /*trick*/) { return {}; }
|
||||
// What the game shows and records of the local skater's tricks (Extension/Skater/trick_rewrite.h):
|
||||
// the ticker's label `from` shown as `to` (empty: that label is the game's again), and the next
|
||||
// trick with one of `from`'s ids, within `within_ms`, recorded as `to`. Empty when done, else why not.
|
||||
virtual std::string trick_label(std::string_view /*plugin*/, std::string_view /*from*/, std::string_view /*to*/) { return unavailable; }
|
||||
virtual std::string trick_record_as(std::string_view /*plugin*/, std::vector<std::uint32_t> /*from*/, std::uint32_t /*to*/, int /*within_ms*/) {
|
||||
return unavailable;
|
||||
}
|
||||
// ---- files: the plugin's own data folder, <game>/Mods/.reskate_plugin_data/<plugin>/ -----------
|
||||
// Paths are relative to it and already checked (plugin_manifest.h's safe_relative_path). A listing
|
||||
// names the files and folders in `folder` (empty: the top), folders ending in "/".
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "Extension/Skater/trick_feed.h"
|
||||
#include "Extension/Skater/skater_push.h"
|
||||
#include "Extension/Skater/trick_gestures.h"
|
||||
#include "Extension/Skater/trick_rewrite.h"
|
||||
#include "Extension/Throwdowns/throwdown_lab.h"
|
||||
#include "Extension/Console/commands.h"
|
||||
#include "Extension/Settings/local_gameplay_settings.h"
|
||||
@@ -857,6 +858,14 @@ public:
|
||||
tricks::prepare_trick_gestures();
|
||||
return tricks::trick_gesture(trick);
|
||||
}
|
||||
std::string trick_label(std::string_view plugin, std::string_view from, std::string_view to) override {
|
||||
return trick_rewrite::label(plugin, from, to);
|
||||
}
|
||||
// A trick recorded as another changes the score: not while a throwdown runs, as trick replacements.
|
||||
std::string trick_record_as(std::string_view plugin, std::vector<std::uint32_t> from, std::uint32_t to, int within_ms) override {
|
||||
if (multiplayer::local_throwdown_active()) return "not while a throwdown runs";
|
||||
return trick_rewrite::record_as(plugin, std::move(from), to, within_ms);
|
||||
}
|
||||
std::string bone_cam(std::string_view plugin, const std::vector<std::string>& words) override {
|
||||
if (words.empty()) return bonecam::bone_cam_setting();
|
||||
const auto& setting = words.front();
|
||||
@@ -991,6 +1000,7 @@ public:
|
||||
hud_corner::release(plugin);
|
||||
game_speed::ask(plugin, 1.0f);
|
||||
grind_exit::release(plugin);
|
||||
trick_rewrite::release(plugin);
|
||||
if (const auto seen = telemetry_seen_.find(plugin); seen != telemetry_seen_.end()) telemetry_seen_.erase(seen);
|
||||
plugin_feed::release_tags(std::string(plugin));
|
||||
if (std::erase_if(binds_, [&](const auto& item) { return item.first.first == plugin; })) publish_binds();
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "trick_rewrite.h"
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/trick_rewrite.h"
|
||||
#include <Windows.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
namespace dingosdk::trick_rewrite {
|
||||
namespace {
|
||||
namespace tr = addr::trick_rewrite;
|
||||
|
||||
using RecordTrick = void (*)(std::uintptr_t recorder, std::uintptr_t packet);
|
||||
using ResolveLocalizedKey = bool (*)(std::uintptr_t object, const char* key, char* buffer, std::uint32_t capacity);
|
||||
|
||||
struct Record {
|
||||
std::vector<std::uint32_t> from;
|
||||
std::uint32_t to{};
|
||||
std::int64_t until{}; // performance-counter ticks
|
||||
};
|
||||
struct State {
|
||||
std::atomic<std::uintptr_t> base{};
|
||||
std::atomic<bool> started{}, label_ready{}, record_ready{};
|
||||
std::atomic<RecordTrick> record_original{};
|
||||
std::atomic<ResolveLocalizedKey> resolve_original{};
|
||||
// Read by the hooks without the lock: whether anything is asked for at all.
|
||||
std::atomic<bool> any_labels{}, any_records{};
|
||||
std::mutex mutex;
|
||||
std::map<std::string, std::map<std::string, std::string, std::less<>>, std::less<>> labels; // owner -> from -> to
|
||||
std::map<std::string, Record, std::less<>> records; // owner -> pending
|
||||
std::atomic<std::uint64_t> labelled{}, recorded{}, no_room{};
|
||||
};
|
||||
State& state() {
|
||||
static auto* value = new State; // process lifetime: the hooks may call in until exit
|
||||
return *value;
|
||||
}
|
||||
struct PreserveError {
|
||||
DWORD value = GetLastError();
|
||||
~PreserveError() { SetLastError(value); }
|
||||
};
|
||||
std::int64_t qpc_now() noexcept {
|
||||
LARGE_INTEGER value{};
|
||||
QueryPerformanceCounter(&value);
|
||||
return value.QuadPart;
|
||||
}
|
||||
std::int64_t qpc_ms(int ms) noexcept {
|
||||
static const std::int64_t frequency = [] {
|
||||
LARGE_INTEGER value{};
|
||||
QueryPerformanceFrequency(&value);
|
||||
return value.QuadPart;
|
||||
}();
|
||||
return frequency * ms / 1000;
|
||||
}
|
||||
void refresh(State& s) {
|
||||
s.any_labels.store(std::any_of(s.labels.begin(), s.labels.end(), [](const auto& owner) { return !owner.second.empty(); }),
|
||||
std::memory_order_release);
|
||||
s.any_records.store(!s.records.empty(), std::memory_order_release);
|
||||
}
|
||||
|
||||
// Game thread(s). The game's method runs first and fills its caller's buffer; only then, and only for
|
||||
// a label asked for on the ticker's route, is the text in that buffer replaced.
|
||||
__declspec(noinline) bool resolve_localized_key(std::uintptr_t object, const char* key, char* buffer, std::uint32_t capacity) {
|
||||
auto& s = state();
|
||||
const bool success = s.resolve_original.load(std::memory_order_acquire)(object, key, buffer, capacity);
|
||||
if (!success || !buffer || !capacity || !s.any_labels.load(std::memory_order_acquire)) return success;
|
||||
PreserveError preserve;
|
||||
char text[most_label_text + 1]{};
|
||||
const auto length = memory::peek_cstring(reinterpret_cast<std::uintptr_t>(buffer), text, sizeof(text));
|
||||
if (length <= 0 || static_cast<std::uint32_t>(length) >= capacity) return success;
|
||||
std::string replacement;
|
||||
{
|
||||
std::unique_lock lock(s.mutex, std::try_to_lock);
|
||||
if (!lock.owns_lock()) return success;
|
||||
for (const auto& [owner, labels] : s.labels)
|
||||
if (const auto found = labels.find(std::string_view(text, static_cast<std::size_t>(length))); found != labels.end()) {
|
||||
replacement = found->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replacement.empty()) return success;
|
||||
// Captured here, in the hook itself: frame 0 is this function, frame 1 its caller.
|
||||
std::array<void*, 2 + tr::ticker_label_route.size()> frames{};
|
||||
const auto count = RtlCaptureStackBackTrace(0, static_cast<ULONG>(frames.size()), frames.data(), nullptr);
|
||||
const auto base = s.base.load(std::memory_order_relaxed);
|
||||
bool route = count == frames.size();
|
||||
for (std::size_t i = 0; route && i < tr::ticker_label_route.size(); ++i)
|
||||
route = reinterpret_cast<std::uintptr_t>(frames[2 + i]) == base + tr::ticker_label_route[i];
|
||||
if (!route) return success;
|
||||
if (capacity <= replacement.size()) { // never cut a label short
|
||||
s.no_room.fetch_add(1, std::memory_order_relaxed);
|
||||
return success;
|
||||
}
|
||||
std::memcpy(buffer, replacement.c_str(), replacement.size() + 1);
|
||||
s.labelled.fetch_add(1, std::memory_order_relaxed);
|
||||
return success;
|
||||
}
|
||||
|
||||
// Game thread. A trick a pending record asks for is recorded as its `to`, through a copy of the
|
||||
// packet; anything else goes to the recorder untouched.
|
||||
__declspec(noinline) void record_trick(std::uintptr_t recorder, std::uintptr_t packet) {
|
||||
auto& s = state();
|
||||
const auto original = s.record_original.load(std::memory_order_acquire);
|
||||
if (s.any_records.load(std::memory_order_acquire) && packet >= 0x10000 &&
|
||||
packet < memory::highest_user_address - tr::trick_packet_size) {
|
||||
PreserveError preserve;
|
||||
std::uint8_t valid{};
|
||||
std::uint32_t id{};
|
||||
if (memory::peek(packet + tr::trick_packet_valid, valid) && valid && memory::peek(packet + tr::trick_packet_id, id)) {
|
||||
std::optional<std::uint32_t> to;
|
||||
{
|
||||
std::lock_guard lock(s.mutex);
|
||||
const auto now = qpc_now();
|
||||
std::erase_if(s.records, [&](const auto& entry) { return now > entry.second.until; });
|
||||
for (auto it = s.records.begin(); it != s.records.end(); ++it)
|
||||
if (std::find(it->second.from.begin(), it->second.from.end(), id) != it->second.from.end()) {
|
||||
to = it->second.to;
|
||||
s.records.erase(it); // once
|
||||
break;
|
||||
}
|
||||
refresh(s);
|
||||
}
|
||||
std::array<unsigned char, tr::trick_packet_size> copy{};
|
||||
if (to && memory::peek(packet, copy)) {
|
||||
std::memcpy(copy.data() + tr::trick_packet_id, &*to, sizeof(*to));
|
||||
s.recorded.fetch_add(1, std::memory_order_relaxed);
|
||||
// The recorder copies the packet before it returns, so the copy may live here.
|
||||
original(recorder, reinterpret_cast<std::uintptr_t>(copy.data()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
original(recorder, packet);
|
||||
}
|
||||
|
||||
template<class Function, std::size_t N>
|
||||
bool install(std::uintptr_t base, std::uintptr_t rva, const std::array<unsigned char, N>& prefix,
|
||||
Function replacement, std::atomic<Function>& original) noexcept {
|
||||
std::array<unsigned char, N> actual{};
|
||||
if (!memory::peek(base + rva, actual) || actual != prefix) return false;
|
||||
auto* const target = reinterpret_cast<void*>(base + rva);
|
||||
void* previous{};
|
||||
if (hook_prepare(target, reinterpret_cast<void*>(replacement), &previous) != HookOk) return false;
|
||||
if (!previous) {
|
||||
(void)hook_remove(target);
|
||||
return false;
|
||||
}
|
||||
original.store(reinterpret_cast<Function>(previous), std::memory_order_release);
|
||||
if (hook_enable(target) != HookOk) {
|
||||
(void)hook_remove(target);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool start(std::uintptr_t image_base) noexcept {
|
||||
auto& s = state();
|
||||
if (s.started.exchange(true, std::memory_order_acq_rel)) return labels_available() || records_available();
|
||||
s.base.store(image_base, std::memory_order_release);
|
||||
s.label_ready.store(install(image_base, tr::resolve_localized_key, tr::resolve_localized_key_prefix, &resolve_localized_key,
|
||||
s.resolve_original), std::memory_order_release);
|
||||
s.record_ready.store(install(image_base, tr::record_trick, tr::record_trick_prefix, &record_trick, s.record_original),
|
||||
std::memory_order_release);
|
||||
logging::log(logging::Level::info, logging::Channel::skater, "Trick labels and records for plugins: ticker labels {}, trick records {}.",
|
||||
s.label_ready.load() ? "hooked" : "unavailable (this build's code differs)",
|
||||
s.record_ready.load() ? "hooked" : "unavailable (this build's code differs)");
|
||||
return s.label_ready.load() || s.record_ready.load();
|
||||
}
|
||||
bool labels_available() noexcept { return state().label_ready.load(std::memory_order_acquire); }
|
||||
bool records_available() noexcept { return state().record_ready.load(std::memory_order_acquire); }
|
||||
|
||||
std::string label(std::string_view owner, std::string_view from, std::string_view to) {
|
||||
if (from.empty() || from.size() > most_label_text || to.size() > most_label_text) return "a label is 1 to 63 characters";
|
||||
auto& s = state();
|
||||
if (!labels_available()) return "trick labels are unavailable for this game build";
|
||||
std::lock_guard lock(s.mutex);
|
||||
auto& mine = s.labels[std::string(owner)];
|
||||
if (to.empty()) mine.erase(std::string(from));
|
||||
else {
|
||||
if (!mine.contains(from) && mine.size() >= most_labels) return "a plugin has at most 16 labels";
|
||||
mine[std::string(from)] = std::string(to);
|
||||
}
|
||||
if (mine.empty()) s.labels.erase(std::string(owner));
|
||||
refresh(s);
|
||||
return {};
|
||||
}
|
||||
std::string record_as(std::string_view owner, std::vector<std::uint32_t> from, std::uint32_t to, int within_ms) {
|
||||
if (from.empty() || from.size() > 16) return "a record names 1 to 16 trick ids";
|
||||
for (const auto id : from)
|
||||
if (id > most_trick_id) return "a trick id is 0 to 336";
|
||||
if (to > most_trick_id) return "a trick id is 0 to 336";
|
||||
if (within_ms < 1 || within_ms > most_within_ms) return "a record waits 1 to 2000 ms";
|
||||
auto& s = state();
|
||||
if (!records_available()) return "trick records are unavailable for this game build";
|
||||
std::lock_guard lock(s.mutex);
|
||||
s.records[std::string(owner)] = Record{std::move(from), to, qpc_now() + qpc_ms(within_ms)};
|
||||
refresh(s);
|
||||
return {};
|
||||
}
|
||||
void release(std::string_view owner) {
|
||||
auto& s = state();
|
||||
std::lock_guard lock(s.mutex);
|
||||
if (const auto found = s.labels.find(owner); found != s.labels.end()) s.labels.erase(found);
|
||||
if (const auto found = s.records.find(owner); found != s.records.end()) s.records.erase(found);
|
||||
refresh(s);
|
||||
}
|
||||
Counts counts() noexcept {
|
||||
const auto& s = state();
|
||||
return {s.labelled.load(), s.recorded.load(), s.no_room.load()};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// What the game shows and records of the local skater's tricks, as plugins ask (reskate.tricks.label
|
||||
// and record_as; the hooks TheRealEJR found for Rock to Fakie, pull request 285):
|
||||
// - A label: the trick ticker shows `to` where the game's text is exactly `from`. Only the label the
|
||||
// ticker asks for on the one call route seen for it (Engine/Game/Build/20260929/trick_rewrite.h),
|
||||
// in the ticker's own buffer and only when it has room: the string database, other text and
|
||||
// other players' tickers are the game's.
|
||||
// - A record: the next trick the recorder gets whose id is one of `from`, within `within_ms`, is
|
||||
// recorded as `to`. The recorder is given a changed copy of the packet, never the game's.
|
||||
// Each owner (a plugin) has its own labels and one pending record; both go with release(). With none
|
||||
// asked for, the hooks pass everything on unchanged. Any thread.
|
||||
namespace dingosdk::trick_rewrite {
|
||||
// Installs the hooks when the game's code matches.
|
||||
bool start(std::uintptr_t image_base) noexcept;
|
||||
bool labels_available() noexcept;
|
||||
bool records_available() noexcept;
|
||||
|
||||
inline constexpr std::size_t most_label_text = 63, most_labels = 16;
|
||||
inline constexpr std::uint32_t most_trick_id = 0x150;
|
||||
inline constexpr int most_within_ms = 2000;
|
||||
|
||||
// Empty when done, else why not. An empty `to` takes that owner's label for `from` away.
|
||||
std::string label(std::string_view owner, std::string_view from, std::string_view to);
|
||||
// Replaces that owner's pending record. Empty when done, else why not.
|
||||
std::string record_as(std::string_view owner, std::vector<std::uint32_t> from, std::uint32_t to, int within_ms);
|
||||
void release(std::string_view owner);
|
||||
|
||||
struct Counts {
|
||||
std::uint64_t labelled{}, recorded{}, no_room{};
|
||||
};
|
||||
Counts counts() noexcept;
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "Extension/UI/Overlay/game_images.h"
|
||||
#include "Extension/Plugins/plugin_runtime.h"
|
||||
#include "Extension/Skater/grind_exit.h"
|
||||
#include "Extension/Skater/trick_rewrite.h"
|
||||
#include "Extension/Skater/camera_observer.h"
|
||||
#include "Extension/Progression/entitlement_request_hook.h"
|
||||
#include "Extension/Skater/skater_observer.h"
|
||||
@@ -464,6 +465,8 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() {
|
||||
std::string(xray ? "true" : "false") + "}");
|
||||
const bool grind_exits = dingosdk::grind_exit::start(r.base);
|
||||
record("{\"event\":\"grind_exit_initialized\",\"active\":" + std::string(grind_exits ? "true" : "false") + "}");
|
||||
const bool trick_rewrites = dingosdk::trick_rewrite::start(r.base);
|
||||
record("{\"event\":\"trick_rewrite_initialized\",\"active\":" + std::string(trick_rewrites ? "true" : "false") + "}");
|
||||
const bool noclip_velocity = dingosdk::start_client_noclip_velocity(r.base);
|
||||
record("{\"event\":\"noclip_velocity_initialized\",\"active\":" + std::string(noclip_velocity ? "true" : "false") + "}");
|
||||
const bool loading_screens = dingosdk::loading_screen::start(r.base);
|
||||
|
||||
@@ -141,6 +141,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/Skater/xray_skeleton.cpp
|
||||
Extension/Skater/game_speed.cpp
|
||||
Extension/Skater/grind_exit.cpp
|
||||
Extension/Skater/trick_rewrite.cpp
|
||||
Extension/UI/hud_corner.cpp
|
||||
Extension/Assets/morph_memory_pool.cpp
|
||||
Extension/Assets/native_render_resource_pool.cpp
|
||||
|
||||
@@ -36,6 +36,9 @@ test`) that `dingosdk_plugin_tests` runs too, and its own page in the ReSkate me
|
||||
- **dual-stick**: the settings of GoalTrains' Dual Stick Controls companion on its page, in the
|
||||
companion's `session_controls.ini` in the plugin's own data folder (`reskate.files`). Made from
|
||||
pull request 254. `dualstick status`, `dualstick preset xl`.
|
||||
- **rock-to-fakie**: TheRealEJR's Rock to Fakie: a Board Stall or Lip Stall shows as Rock to Fakie,
|
||||
and left with B is recorded as the game's fakie_rock (`reskate.tricks.label`, `record_as`). From
|
||||
pull request 285. `rocktofakie on`.
|
||||
|
||||
## Throwdown modes
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Rock to Fakie (plugin)
|
||||
|
||||
TheRealEJR's [Rock to Fakie](https://github.com/TheRealEJR/ReSkate-Rock-to-Fakie-mod) as a
|
||||
sandboxed Luau plugin, from his pull request 285. skate. has no button for a rock to fakie, but the
|
||||
move is in the game: ride up to a coping, Board Stall or Lip Stall on it, and press **B** (Circle on
|
||||
a PlayStation pad) to roll back in fakie. With this on, the game calls it what it is:
|
||||
|
||||
- **The trick ticker** shows Rock to Fakie for a Board Stall or a Lip Stall while the skater holds
|
||||
it (`reskate.tricks.label`). A BS / FS prefix the ticker adds is the game's and stays.
|
||||
- **A stall left with B** is recorded as the game's own `fakie_rock` (trick 187) instead of the stall
|
||||
(9 or 12): B pressed while the stall is held asks that the stall the game records within 250 ms be
|
||||
recorded as `fakie_rock` (`reskate.tricks.record_as`).
|
||||
|
||||
The stall is the physics state the game chose (`reskate.skater.telemetry().watch.state`): 400 or 401
|
||||
holds a stall or a grind, and only a stall's label or trick id is ever changed. Off until switched
|
||||
on: `rocktofakie on|off|status`, or its page in the ReSkate menu's PLUGINS section.
|
||||
|
||||
## Install
|
||||
|
||||
Copy this folder into the game's `Mods` folder.
|
||||
|
||||
## Limits
|
||||
|
||||
- The hooks behind `reskate.tricks` are tied to the supported game build: on another, the status
|
||||
says why nothing is asked.
|
||||
- It does not detect a real coping contact: any Board Stall or Lip Stall counts, wherever it is done.
|
||||
- A recorded trick is your game's: whether a multiplayer host sees it the same has not been checked.
|
||||
It is not asked while a throwdown runs.
|
||||
@@ -0,0 +1,81 @@
|
||||
-- Rock to Fakie, by TheRealEJR (his Rock to Fakie mod, TheRealEJR/ReSkate-Rock-to-Fakie-mod, ported
|
||||
-- to ReSkate in pull request 285), as a plugin. skate. has no button for a rock to fakie, but the
|
||||
-- move is in the game: ride up to a coping, Board Stall or Lip Stall on it, and press B to roll back
|
||||
-- in fakie. With this on, the game calls it what it is:
|
||||
-- - the trick ticker shows Rock to Fakie for a Board Stall or a Lip Stall while the skater holds
|
||||
-- it (reskate.tricks.label, only while the stall is held);
|
||||
-- - a stall left with B is recorded as the game's own fakie_rock (reskate.tricks.record_as: B
|
||||
-- pressed while the stall is held asks that the stall the game records within a moment of it be
|
||||
-- recorded as fakie_rock).
|
||||
-- The stall is the physics state the game chose (reskate.skater.telemetry's watch): 400 or 401
|
||||
-- holds a stall or a grind, and only a stall's label or trick id is changed. Off until switched on.
|
||||
local held_states = {[400] = true, [401] = true}
|
||||
local board_stall, lip_stall, fakie_rock = 9, 12, 187 -- the game's trick ids (bs_board, bs_lip, fakie_rock)
|
||||
local stall_labels = {"Board Stall", "Lip Stall"}
|
||||
local name = "Rock to Fakie"
|
||||
local b_button = 0x2000 -- XInput B, Circle on a PlayStation pad
|
||||
-- B pressed while the stall is held, then the stall left: the game records it a moment later.
|
||||
local record_window_ms = 250
|
||||
|
||||
local on = reskate.storage.get("on") == true
|
||||
local labelled = false
|
||||
local b_down = false
|
||||
local asked, refused = 0, nil
|
||||
|
||||
local function show_labels(show)
|
||||
if show == labelled then return end
|
||||
labelled = show
|
||||
for _, label in stall_labels do
|
||||
local ok, why = reskate.tricks.label(label, if show then name else nil)
|
||||
if not ok then refused = why end
|
||||
end
|
||||
end
|
||||
|
||||
reskate.on("tick", function()
|
||||
if not on then
|
||||
show_labels(false)
|
||||
b_down = false
|
||||
return
|
||||
end
|
||||
local t = reskate.skater.telemetry()
|
||||
local watch = t and t.watch
|
||||
local stalled = watch ~= nil and watch.valid and held_states[watch.state] == true
|
||||
show_labels(stalled)
|
||||
local pad = reskate.input.pad_bits()
|
||||
local down = pad ~= nil and bit32.band(pad, b_button) ~= 0
|
||||
if down and not b_down and stalled then
|
||||
local ok, why = reskate.tricks.record_as({board_stall, lip_stall}, fakie_rock, record_window_ms)
|
||||
if ok then asked += 1 else refused = why end
|
||||
end
|
||||
b_down = down
|
||||
end)
|
||||
|
||||
local function set_on(value)
|
||||
on = value
|
||||
reskate.storage.set("on", value)
|
||||
if not value then show_labels(false) end
|
||||
end
|
||||
local function status()
|
||||
return string.format("%s. %d rock to fakie(s) asked for this game%s.", if on then "on" else "off", asked,
|
||||
if refused then "; last refusal: " .. refused else "")
|
||||
end
|
||||
|
||||
reskate.console.command("rocktofakie", "Rock to Fakie: on|off|status", function(args)
|
||||
local word = args[1] or "status"
|
||||
if word == "on" or word == "1" then set_on(true)
|
||||
elseif word == "off" or word == "0" then set_on(false)
|
||||
elseif word ~= "status" then return reskate.console.print("rocktofakie: say on, off or status") end
|
||||
reskate.console.print("rocktofakie: " .. status())
|
||||
end)
|
||||
|
||||
reskate.menu.page("Rock to Fakie", function(ui)
|
||||
ui:card("ROCK TO FAKIE", "Board Stall or Lip Stall, then B")
|
||||
local value = ui:toggle("on", "Rock to Fakie", on,
|
||||
"Ride up to a coping, Board Stall or Lip Stall on it, and press B to roll back in fakie: the ticker shows Rock to Fakie, and the trick is recorded as one.")
|
||||
if value ~= on then set_on(value) end
|
||||
ui:note("Any Board Stall or Lip Stall counts, wherever it is done: the game does not say whether it is on a coping.")
|
||||
ui:info("Status", status())
|
||||
end)
|
||||
|
||||
reskate.on("unload", function() show_labels(false) end)
|
||||
reskate.log("Rock to Fakie " .. (if on then "on" else "off"))
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"api": 1,
|
||||
"entry": "plugin/main.lua",
|
||||
"side": "client",
|
||||
"permissions": ["skater.read", "skater.control", "input", "hud", "storage", "menu", "console"]
|
||||
}
|
||||
Reference in new issue
Block a user