Files
AyeZeeBB 0dcbcb3a89 Drone: other players' drones shown as a 3D model
A model is spawned at each other player's flying drone and moved every frame. It is the drone's
own model (FPV Drone by PinaPipe, CC BY 4.0, credited on the DRONE page), built by ReSkate Studio
into a pack that ReSkate.dll carries and unpacks into Mods/.reskate-builtin before the merge.
Without the pack the game's traffic cone stands in. dronemodeltest spawns one in front of you.
2026-10-15 22:39:49 -05:00

359 lines
16 KiB
C++

#include "native_prop.h"
#include "native_skater_internal.h"
#include "native_creation_list.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Abi/native_data.h"
#include "Engine/Game/Build/20260929/engine.h"
#include "Engine/Game/Build/20260929/world_props.h"
#include "Extension/Skater/drone_beacon.h"
#include <cmath>
#include <intrin.h>
#include <optional>
namespace dingosdk::multiplayer {
using namespace native_skater_detail;
namespace props = addr::world_props;
namespace {
// ---- Placed props, and the engine taking them away
// Every prop entity placed and not yet destroyed. The engine's destruction hook takes an entity
// out before it is freed, under the same lock that every move and removal holds, so nothing
// moves a freed prop. (Recursive: a removal is a destruction of our own.)
struct Registry {
std::recursive_mutex mutex;
std::vector<std::uintptr_t> live;
std::atomic<std::size_t> count{};
};
Registry &registry() {
static auto *r = new Registry;
return *r;
}
bool tracked(std::uintptr_t entity) {
auto &r = registry();
return entity && std::find(r.live.begin(), r.live.end(), entity) != r.live.end();
}
void track(std::uintptr_t entity) {
auto &r = registry();
r.live.push_back(entity);
r.count.store(r.live.size(), std::memory_order_release);
}
void untrack(std::uintptr_t entity) {
auto &r = registry();
std::erase(r.live, entity);
r.count.store(r.live.size(), std::memory_order_release);
}
struct Prop {
std::uintptr_t entity{}, parent{}, context{};
};
// A blueprint the way the cone's is one (world_props.h), or the skateboard's or the skater
// code's; `chain` gets what it was, for the log.
bool is_blueprint(std::uintptr_t base, std::uintptr_t object, std::string &chain) {
const auto vtable = ptr(object);
chain = std::format("vtable Skate+{:#x}", vtable - base);
if (vtable == base + props::object_blueprint_vtable || vtable == base + addr::engine::blueprint_vtable) return true;
auto type = ptr(object, 8);
for (int depth = 0; depth < 8 && type; ++depth) {
if (type == base + addr::engine::board_blueprint_type || type == base + entities::blueprint_type) return true;
const auto super = ptr(type, 0x20);
if (super == type) break;
type = super;
}
return false;
}
// A loaded named blueprint, from whichever asset domain holds it; 0 when none does.
std::uintptr_t find_named_blueprint(std::uintptr_t base, std::string_view name) {
const auto find = game::native_data().find_asset;
require(find != nullptr, "Native asset lookup is unavailable.");
const std::string text(name);
for (std::uint16_t domain = 0; domain < 0xbbf; ++domain) {
if (!ptr(base, addr::engine::domain_owners + domain * std::uintptr_t{8})) continue;
if (const auto asset = find(domain, text.c_str())) {
std::string chain;
if (!is_blueprint(base, asset, chain))
throw std::runtime_error(std::format("{} is not a blueprint ({}).", name, chain));
return asset;
}
}
return 0;
}
// Whether entities can be created now, on this thread: the hooks are in, the engine is ready
// and the client's job context is this one. Throws only when something is wrong; false is "wait".
bool can_create(std::uintptr_t base, const NativeFrame &local) {
install(base);
require(shared().hooks, "The native entity hooks are not installed.");
require(GetCurrentThreadId() == shared().engine_thread, "Props are created on the client thread.");
if (read<std::uint8_t>(base, addr::engine::entity_creation_ready) == 0) return false;
const auto tls_index = read<std::uint32_t>(base, addr::engine::tls_index);
require(tls_index <= 4095, "Native TLS index changed.");
const auto tls = ptr(static_cast<std::uintptr_t>(__readgsqword(0x58)), std::uintptr_t{tls_index} * 8);
return read<std::uint8_t>(tls, 0xb19) != 0 && ptr(tls, 0x550) == local.context;
}
// As the remote skateboard is made (native_skater_spawn.cpp): a fresh entity through a creation
// list, the list initialized, then placed with its lifecycle callbacks. `kinds` gets the vtable
// of each entity the list made. Under the registry lock.
Prop create_prop(std::uintptr_t base, const NativeFrame &local, std::uintptr_t blueprint, const std::array<float, 16> &where,
std::string *kinds = nullptr) {
alignas(16) const auto matrix = where;
NativeCreationList list{};
alignas(16) std::array<std::uint8_t, 0x190> descriptor{};
using Init = void *(*)(void *, std::uintptr_t, std::uintptr_t, const void *);
reinterpret_cast<Init>(base + entities::descriptor_init)(descriptor.data(), 0, local.parent, matrix.data());
struct DescriptorScope {
std::uintptr_t base;
void *data;
~DescriptorScope() { reinterpret_cast<void (*)(void *)>(base + entities::descriptor_destroy)(static_cast<std::uint8_t *>(data) + 0x10); }
} scope{base, descriptor.data()};
const std::uint32_t id = 255; // local, unassociated
std::memcpy(descriptor.data() + 0x38, &id, sizeof(id));
descriptor[0x151] = 0;
auto *list_pointer = &list;
std::memcpy(descriptor.data() + 0x158, &list_pointer, sizeof(list_pointer));
std::array<std::uintptr_t, 3> result{};
using Create = void *(*)(void *, void *, std::uintptr_t, std::uintptr_t, std::uintptr_t);
reinterpret_cast<Create>(base + entities::create_entity)(result.data(), descriptor.data(), blueprint, 0, 0);
if (result[1]) reinterpret_cast<void (*)(std::uintptr_t)>(base + entities::release_reference)(result[1]);
const auto created = read_native_creation_list(readable, list);
if (kinds)
for (const auto entity : created.entities) *kinds += std::format(" Skate+{:#x}", ptr(entity) - base);
if (!created.pages.empty()) reinterpret_cast<void (*)(void *)>(base + native::initialize_creation_list)(&list);
for (const auto page : created.pages) {
const std::uint32_t empty = 0;
(void)write(page + 8, &empty, sizeof(empty));
reinterpret_cast<void (*)(void *, std::uintptr_t, std::size_t)>(base + native::free_creation_page)(&list, page, 0x1f0);
}
require(result[0] != 0, "The engine did not create the prop.");
using InitializePlacement = void (*)(std::uintptr_t, const void *, std::uintptr_t, std::uint8_t);
reinterpret_cast<InitializePlacement>(base + entities::initialize_placement)(result[0], matrix.data(), 0, 1);
track(result[0]);
return {result[0], ptr(result[0], 0x40), local.context};
}
// The plain entity transform setter: what moves a prop smoothly (world_props.h). Under the lock.
void place(std::uintptr_t base, const Prop &prop, const std::array<float, 16> &where) {
alignas(16) const auto matrix = where;
reinterpret_cast<void (*)(std::uintptr_t, const void *)>(base + entities::place_entity)(prop.entity, matrix.data());
}
// Under the lock. One the engine already took is only forgotten.
void destroy(const Prop &prop) {
if (!tracked(prop.entity)) return;
untrack(prop.entity);
if (shared().hooks && ptr(prop.entity, 0x20) == prop.context) shared().original_destroy(prop.entity, prop.parent);
}
std::array<float, 16> matrix_of(const std::array<float, 3> &position, const std::array<float, 4> &rotation) {
Transform t;
t.position = position;
t.rotation = rotation;
return to_matrix(t);
}
// ---- Other players' drones
struct DroneModel {
std::uint64_t id{};
Prop prop;
};
struct Models {
std::vector<DroneModel> list; // client thread
std::uintptr_t context{}, blueprint{}; // the map the models are on, and what they are made of
std::string blueprint_name;
ULONGLONG next_lookup{};
std::mutex status_mutex;
std::string status = "No other player is flying a drone.";
};
Models &models() {
static auto *m = new Models;
return *m;
}
void set_models_status(std::string text, bool log) {
auto &m = models();
{
std::lock_guard lock(m.status_mutex);
if (m.status == text) return;
m.status = text;
}
if (log) logging::log(logging::Level::info, logging::Channel::runtime, "Drone models: {}", text);
}
// ---- The test that found how
struct Request {
std::string blueprint; // empty: stop
int mode{};
};
struct PropTest {
std::mutex mutex;
std::optional<Request> request;
std::string status = "Not running. Console: dronemodeltest [blueprint] [mode 0-2], dronemodeltest stop";
Prop prop; // client thread
std::array<float, 3> anchor{}, position{};
ULONGLONG started{};
unsigned frames{};
int mode{};
};
PropTest &test() {
static auto *t = new PropTest;
return *t;
}
void set_test_status(std::string text) {
logging::log(logging::Level::info, logging::Channel::runtime, "Drone model test: {}", text);
std::lock_guard lock(test().mutex);
test().status = std::move(text);
}
std::array<float, 16> yawed(const std::array<float, 3> &position, float yaw) {
return matrix_of(position, {0.0f, std::sin(yaw / 2), 0.0f, std::cos(yaw / 2)});
}
void test_spawn(std::uintptr_t base, std::uintptr_t client, const Request &request) {
auto &t = test();
const auto local = capture_local(base, client, false);
require(local.ready && local.context && local.parent && local.entity, "The local skater is not ready.");
require(can_create(base, local), "Waiting for the engine to allow creating entities; try again.");
const auto blueprint = find_named_blueprint(base, request.blueprint);
require(blueprint != 0, "That blueprint is not loaded on this map.");
const auto root = to_matrix(local.pose.root);
const std::array<float, 3> at{local.pose.root.position[0] - root[8] * 4.0f, local.pose.root.position[1] + 2.0f,
local.pose.root.position[2] - root[10] * 4.0f};
std::string kinds;
t.prop = create_prop(base, local, blueprint, yawed(at, 0.0f), &kinds);
t.anchor = at;
t.started = GetTickCount64();
t.frames = 0;
t.mode = request.mode;
set_test_status(std::format("spawned {} at ({:.1f}, {:.1f}, {:.1f}): entity {:#x} (vtable Skate+{:#x}), made:{}; moving it with mode {}.",
request.blueprint, at[0], at[1], at[2], t.prop.entity, ptr(t.prop.entity) - base, kinds, t.mode));
}
// Round a 3 m circle every 6 s, turning to face along it.
void test_move(std::uintptr_t base) {
auto &t = test();
const float seconds = static_cast<float>(GetTickCount64() - t.started) / 1000.0f;
const float angle = seconds * 6.2831853f / 6.0f;
const std::array<float, 3> at{t.anchor[0] + 3.0f * std::cos(angle), t.anchor[1] + 0.5f * std::sin(angle * 2.0f),
t.anchor[2] + 3.0f * std::sin(angle)};
const auto matrix = yawed(at, -angle);
t.position = at;
if (t.mode >= 1 && t.prop.parent) {
const auto transform = ptr(t.prop.parent, 0x18);
alignas(16) const auto copy = matrix;
if (transform && ptr(transform) == base + native::blueprint_transform_vtable)
reinterpret_cast<void (*)(std::uintptr_t, const void *)>(base + native::set_blueprint_transform)(transform, copy.data());
}
if (t.mode <= 1) place(base, t.prop, matrix);
if (++t.frames == 120) set_test_status(std::format("moved 120 frames with mode {}. Is it going round a circle?", t.mode));
}
} // namespace
void prop_entity_destroyed(std::uintptr_t entity) noexcept {
auto &r = registry();
if (!r.count.load(std::memory_order_acquire)) return; // every destruction passes through here
std::lock_guard lock(r.mutex);
if (tracked(entity)) untrack(entity);
}
void tick_drone_models(std::uintptr_t base, std::uintptr_t client, bool show) noexcept {
auto &m = models();
try {
std::lock_guard lock(registry().mutex);
// The engine took some away (a map change takes them all).
std::erase_if(m.list, [](const DroneModel &model) { return !tracked(model.prop.entity); });
const auto drones = show ? drone::beacon::snapshot() : std::vector<drone::beacon::Remote>{};
if (drones.empty() && m.list.empty()) {
set_models_status(show ? "No other player is flying a drone." : "Other players' drones are hidden.", false);
return;
}
const auto local = capture_local(base, client, false);
if (!local.ready || !local.context || !local.parent) return;
if (local.context != m.context) {
// Another map: whatever was there went with it, and the blueprint is found again.
m.list.clear();
m.context = local.context;
m.blueprint = 0;
m.next_lookup = 0;
}
// Those who landed or left.
for (auto it = m.list.begin(); it != m.list.end();) {
const bool flying = std::any_of(drones.begin(), drones.end(), [&](const auto &d) { return d.id == it->id; });
if (flying) {
++it;
continue;
}
destroy(it->prop);
it = m.list.erase(it);
}
// What the models are made of: the drone's own model, else the stand-in.
const auto now = GetTickCount64();
if (!m.blueprint && !drones.empty() && now >= m.next_lookup) {
m.next_lookup = now + 5000;
for (const auto name : {drone_model_blueprint, drone_model_stand_in})
if ((m.blueprint = find_named_blueprint(base, name))) {
m.blueprint_name = std::string(name);
break;
}
if (!m.blueprint)
set_models_status("Neither the drone model nor its stand-in is loaded on this map: other players' drones show as markers only.", true);
}
// One new model a frame at most, so a lobby taking off together does not stall a frame.
bool spawned = false;
for (const auto &drone : drones) {
const auto matrix = matrix_of(drone.position, drone.rotation);
const auto found = std::find_if(m.list.begin(), m.list.end(), [&](const DroneModel &model) { return model.id == drone.id; });
if (found != m.list.end()) {
place(base, found->prop, matrix);
} else if (m.blueprint && !spawned && m.list.size() < drone::beacon::max_remote && can_create(base, local)) {
m.list.push_back({drone.id, create_prop(base, local, m.blueprint, matrix)});
spawned = true;
}
}
if (m.blueprint)
set_models_status(std::format("{} other player(s) flying; {} model(s) shown, made of {}.", drones.size(), m.list.size(),
m.blueprint_name), spawned);
} catch (const std::exception &e) {
// Give up on what is there; the next frame starts over.
for (const auto &model : m.list) destroy(model.prop);
m.list.clear();
m.blueprint = 0;
m.next_lookup = GetTickCount64() + 5000;
set_models_status(std::format("failed: {}", e.what()), true);
}
}
std::string drone_models_status() {
auto &m = models();
std::lock_guard lock(m.status_mutex);
return m.status;
}
void request_prop_test(std::string blueprint, int mode) {
std::lock_guard lock(test().mutex);
test().request = Request{std::move(blueprint), std::clamp(mode, 0, 2)};
}
std::optional<std::array<float, 3>> prop_test_position() noexcept {
const auto &t = test();
if (!t.prop.entity) return std::nullopt;
return t.position;
}
std::string prop_test_status() {
std::lock_guard lock(test().mutex);
return test().status;
}
void tick_prop_test(std::uintptr_t base, std::uintptr_t client) noexcept {
auto &t = test();
std::optional<Request> request;
{
std::lock_guard lock(t.mutex);
request.swap(t.request);
}
try {
std::lock_guard lock(registry().mutex);
if (t.prop.entity && !tracked(t.prop.entity)) {
t.prop = {};
set_test_status("the engine took the prop away (a map change).");
}
if (request) {
if (t.prop.entity) destroy(t.prop);
t.prop = {};
if (request->blueprint.empty()) set_test_status("stopped; the prop is removed.");
else test_spawn(base, client, *request);
}
if (t.prop.entity) test_move(base);
} catch (const std::exception &e) {
t.prop = {};
set_test_status(std::format("failed: {}", e.what()));
}
}
} // namespace dingosdk::multiplayer