#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 #include #include 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 live; std::atomic count{}; }; Registry ®istry() { 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(base, addr::engine::entity_creation_ready) == 0) return false; const auto tls_index = read(base, addr::engine::tls_index); require(tls_index <= 4095, "Native TLS index changed."); const auto tls = ptr(static_cast(__readgsqword(0x58)), std::uintptr_t{tls_index} * 8); return read(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 &where, std::string *kinds = nullptr) { alignas(16) const auto matrix = where; NativeCreationList list{}; alignas(16) std::array descriptor{}; using Init = void *(*)(void *, std::uintptr_t, std::uintptr_t, const void *); reinterpret_cast(base + entities::descriptor_init)(descriptor.data(), 0, local.parent, matrix.data()); struct DescriptorScope { std::uintptr_t base; void *data; ~DescriptorScope() { reinterpret_cast(base + entities::descriptor_destroy)(static_cast(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 result{}; using Create = void *(*)(void *, void *, std::uintptr_t, std::uintptr_t, std::uintptr_t); reinterpret_cast(base + entities::create_entity)(result.data(), descriptor.data(), blueprint, 0, 0); if (result[1]) reinterpret_cast(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(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(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(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 &where) { alignas(16) const auto matrix = where; reinterpret_cast(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 matrix_of(const std::array &position, const std::array &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 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; std::string status = "Not running. Console: dronemodeltest [blueprint] [mode 0-2], dronemodeltest stop"; Prop prop; // client thread std::array 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 yawed(const std::array &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 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(GetTickCount64() - t.started) / 1000.0f; const float angle = seconds * 6.2831853f / 6.0f; const std::array 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(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{}; 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> 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; { 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