mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 15:16:02 +02:00
- contrib/plugins/reskate-test calls every plugin API and says what worked: ptest all, ptest <module>, ptest list. Its 45 checks cover the base set, timers, the level, package files, JSON, require, the console, storage and the sandbox. It is the example to copy for a plugin. - contrib/plugins/reskate-hostile attacks the sandbox, one attack per command (hostile list): loops, memory and buffer bombs, caught errors, error and log floods, timers, recursion, an unload that never ends, and 17 ways out. Each must end stopped or refused. - dingosdk_plugin_tests runs both from the repository: every reskate-test check passes, and every hostile attack is stopped or refused with nothing escaping. - Taken from Domination (PR 279) for the plugin API to use: the trick watch (when a trick starts, lands and what it scored, or a slam breaks it; Domination's spots left out) in Extension/Skater/trick_watch.h with its tests, the score HUD reader in score_hud.h, and teleport_local_skater_heading, whose hunks are the PR's own. Neither is wired to anything yet.
142 lines
5.1 KiB
C++
142 lines
5.1 KiB
C++
#include "score_hud.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
#include "Engine/Game/Abi/native_data.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include "Engine/Game/Build/20260929/hud_corner.h"
|
|
#include "Extension/UI/NativeMenu/native_menu_data.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <format>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
namespace dingosdk::tricks {
|
|
namespace {
|
|
namespace menu_data = multiplayer::menu_data;
|
|
using Address = std::uintptr_t;
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
// ScoringHUDViewModel's fields, by the hashes of their names in the game's own type data
|
|
// (ui/features/hud/scoring/ScoringHUDViewModel; HudWidgetActive 0x3aa2f739 and ExtraInfoStyle
|
|
// 0x0426783f are the ones hud_corner.h already uses).
|
|
struct Watched {
|
|
const char* name;
|
|
std::uint32_t hash;
|
|
};
|
|
enum : std::size_t { line_score, is_slam, multiplier, action_active };
|
|
constexpr std::array<Watched, 4> watched{{
|
|
{"LineScore", 1086322279u},
|
|
{"IsSlam", 2943226809u},
|
|
{"Multiplier", 1782700230u},
|
|
{"ActionActive", 2058053468u},
|
|
}};
|
|
constexpr std::size_t value_bytes = 16; // read per field
|
|
|
|
struct Bound {
|
|
bool present{};
|
|
std::uint16_t offset{};
|
|
unsigned size{}, kind{};
|
|
std::array<unsigned char, value_bytes> now{};
|
|
bool read{};
|
|
};
|
|
struct Hud {
|
|
Clock::time_point next_look;
|
|
Address manager{};
|
|
menu_data::Value model;
|
|
std::array<Bound, watched.size()> fields{};
|
|
bool warned{};
|
|
};
|
|
Hud hud; // client thread
|
|
|
|
// A score field's value: the game's widget logic keeps scores as floats (kind 19, as LineTimer
|
|
// and Multiplier); an integer reading is taken if the float one is not sensible.
|
|
float score_of(const Bound& field) {
|
|
if (!field.present || !field.read || field.size != 4) return 0;
|
|
float f{};
|
|
std::int32_t i{};
|
|
std::memcpy(&f, field.now.data(), 4);
|
|
std::memcpy(&i, field.now.data(), 4);
|
|
if (std::isfinite(f) && (f == 0 || (std::abs(f) > 1e-3f && std::abs(f) < 1e9f))) return f;
|
|
return static_cast<float>(i);
|
|
}
|
|
bool flag_of(const Bound& field) { return field.present && field.read && field.now[0] != 0; }
|
|
|
|
// Runs `use(context)` on the game's UI model registry under its model lock (as Hall of Meat's
|
|
// HUD code does). False while there is no registry.
|
|
template <class Use> bool with_ui_models(Address base, Use&& use) {
|
|
if (!base) return false;
|
|
const auto ui = menu_data::read<Address>(base + addr::engine::ui_manager);
|
|
const auto manager = ui ? menu_data::read<Address>(ui + 0x140) : 0;
|
|
if (!manager) return false;
|
|
const menu_data::Context context(base, manager);
|
|
game::ModelWriteLock lock(manager);
|
|
use(context);
|
|
return true;
|
|
}
|
|
|
|
bool bind_model(Hud& h, const menu_data::Context& context) {
|
|
if (h.manager != context.manager) {
|
|
h.manager = context.manager;
|
|
h.model = {};
|
|
}
|
|
if (h.model.handle && context.type_of(h.model.handle) == h.model.type) return true;
|
|
h.model = {};
|
|
const auto now = Clock::now();
|
|
if (now < h.next_look) return false;
|
|
h.next_look = now + std::chrono::seconds(1);
|
|
const auto roots = context.roots({addr::hud_corner::score_view_schema});
|
|
if (roots.empty()) return false;
|
|
const auto model = roots.front().model;
|
|
std::string layout;
|
|
bool any = false;
|
|
for (std::size_t n = 0; n < watched.size(); ++n) {
|
|
auto& field = h.fields[n];
|
|
field = {};
|
|
try {
|
|
const auto member = context.member(model.type, watched[n].hash);
|
|
field.present = any = true;
|
|
field.offset = member.offset;
|
|
field.size = menu_data::size(member.type);
|
|
field.kind = menu_data::kind(member.type);
|
|
layout += std::format(" {}@+0x{:x}:{}b/k{}", watched[n].name, field.offset, field.size, field.kind);
|
|
} catch (const std::exception&) {
|
|
layout += std::format(" {}:missing", watched[n].name);
|
|
}
|
|
}
|
|
if (!any) return false;
|
|
h.model = model;
|
|
logging::write(logging::Level::info, logging::Channel::skater, "Score HUD model found:" + layout);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
std::optional<ScoreHud> read_score_hud(std::uintptr_t image_base) noexcept {
|
|
auto& h = hud;
|
|
bool ok = false;
|
|
try {
|
|
with_ui_models(image_base, [&](const menu_data::Context& context) {
|
|
if (!bind_model(h, context)) return;
|
|
const auto at = context.address(h.model);
|
|
for (auto& field : h.fields)
|
|
field.read = field.present && memory::peek_bytes(at + field.offset, field.now.data(), std::min<unsigned>(field.size, value_bytes));
|
|
ok = true;
|
|
});
|
|
h.warned = false;
|
|
} catch (const std::exception& error) {
|
|
h.model = {};
|
|
if (!std::exchange(h.warned, true))
|
|
logging::log(logging::Level::warning, logging::Channel::skater, "The score HUD cannot be read: {}", error.what());
|
|
} catch (...) {
|
|
h.model = {};
|
|
}
|
|
if (!ok) return std::nullopt;
|
|
const auto& f = h.fields;
|
|
return ScoreHud{flag_of(f[action_active]), flag_of(f[is_slam]), score_of(f[line_score]), score_of(f[multiplier])};
|
|
}
|
|
}
|