mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-20 00:35:22 +02:00
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.
This commit is contained in:
1 parent
26ec1da5eb
commit
0dcbcb3a89
18 files changed
+724
-20
No files matched your search
@@ -29,3 +29,7 @@ analysis/raw/**/*.res
|
||||
/analysis/
|
||||
/docs/
|
||||
/release
|
||||
|
||||
# Drone model sources (assets/models/fpv/CREDITS.md): large; only the prepared drone.glb is kept.
|
||||
/assets/models/fpv/dji_avata2.glb
|
||||
/assets/models/fpv/fpv_drone.glb
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Props spawned by ReSkate itself (Extension/Multiplayer/Remote/native_prop.cpp): an object
|
||||
// blueprint loaded on the map, created as a client-only entity the way the remote skateboard is
|
||||
// (skater_entities.h, native_skater.h) and moved every frame by the plain entity transform setter
|
||||
// (skater_entities::place_entity). Found in game on 2026-10-15 with the traffic cone
|
||||
// (world/props/common/construction/cmn_prp_trafficcone_01/cmn_prp_trafficcone_01): the creation
|
||||
// list made one entity, and only place_entity moved it smoothly (the blueprint transform did not).
|
||||
namespace dingosdk::game::build::v20260929::world_props {
|
||||
// An ObjectBlueprint's vtable (the cone's). The skater code's blueprint vtable is
|
||||
// engine::blueprint_vtable; an object blueprint has its own.
|
||||
inline constexpr std::uintptr_t object_blueprint_vtable = 0x638dfa8;
|
||||
// The entity a static-model object blueprint creates on the client (the cone's).
|
||||
inline constexpr std::uintptr_t static_model_entity_vtable = 0x6403720;
|
||||
} // namespace dingosdk::game::build::v20260929::world_props
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "builtin_packs.h"
|
||||
#include <Windows.h>
|
||||
#include <miniz.h>
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
|
||||
namespace dingosdk::mods {
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
// Each pack: its RCDATA resource in ReSkate.dll (cmake/Runtime.cmake) and its folder name.
|
||||
struct Embedded {
|
||||
const wchar_t* resource;
|
||||
const char* name;
|
||||
};
|
||||
constexpr Embedded embedded[]{{L"BUILTIN_PACK_FPV_DRONE", "fpv_drone"}};
|
||||
constexpr std::uint64_t max_pack_bytes = 512ull * 1024 * 1024;
|
||||
constexpr std::string_view version_file = ".builtin-version";
|
||||
|
||||
struct Source {
|
||||
HMODULE module{};
|
||||
bool owned{};
|
||||
Source() = default;
|
||||
Source(const Source&) = delete;
|
||||
Source& operator=(const Source&) = delete;
|
||||
~Source() {
|
||||
if (owned && module) FreeLibrary(module);
|
||||
}
|
||||
};
|
||||
bool carries_packs(HMODULE module) {
|
||||
return module && FindResourceW(module, embedded[0].resource, MAKEINTRESOURCEW(10) /* RT_RCDATA */);
|
||||
}
|
||||
// ReSkate.dll: this module when it is the DLL (the game), else ReSkate.dll beside the program
|
||||
// (the launcher, which sits beside Skate.exe and the DLL), as data only.
|
||||
void open_source(Source& source) {
|
||||
HMODULE self{};
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&carries_packs), &self) && carries_packs(self)) {
|
||||
source.module = self;
|
||||
return;
|
||||
}
|
||||
if (const auto loaded = GetModuleHandleW(L"ReSkate.dll"); carries_packs(loaded)) {
|
||||
source.module = loaded;
|
||||
return;
|
||||
}
|
||||
std::wstring program(32768, L'\0');
|
||||
const auto length = GetModuleFileNameW(nullptr, program.data(), static_cast<DWORD>(program.size()));
|
||||
if (!length || length >= program.size()) return;
|
||||
program.resize(length);
|
||||
const auto dll = fs::path(program).parent_path() / L"ReSkate.dll";
|
||||
source.module = LoadLibraryExW(dll.c_str(), nullptr, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
|
||||
source.owned = source.module != nullptr;
|
||||
}
|
||||
std::span<const unsigned char> resource_bytes(HMODULE module, const wchar_t* name) {
|
||||
const auto resource = FindResourceW(module, name, MAKEINTRESOURCEW(10));
|
||||
if (!resource) return {};
|
||||
const auto loaded = LoadResource(module, resource);
|
||||
const auto size = SizeofResource(module, resource);
|
||||
const auto* data = loaded ? static_cast<const unsigned char*>(LockResource(loaded)) : nullptr;
|
||||
return data && size ? std::span<const unsigned char>(data, size) : std::span<const unsigned char>{};
|
||||
}
|
||||
// Which build of a pack this is: enough to tell when the one embedded changed.
|
||||
std::string version_of(std::span<const unsigned char> bytes) {
|
||||
std::uint64_t hash = 0xcbf29ce484222325ULL;
|
||||
for (const auto byte : bytes) hash = (hash ^ byte) * 0x100000001b3ULL;
|
||||
return std::format("{:016x}-{}", hash, bytes.size());
|
||||
}
|
||||
std::string read_text(const fs::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
// A path inside the pack: relative, forward slashes, no "..", plain characters.
|
||||
bool safe_entry(std::string_view name) {
|
||||
if (name.empty() || name.size() > 260 || name.front() == '/' || name.find(':') != std::string_view::npos ||
|
||||
name.find('\\') != std::string_view::npos)
|
||||
return false;
|
||||
std::size_t start = 0;
|
||||
while (start <= name.size()) {
|
||||
const auto end = std::min(name.find('/', start), name.size());
|
||||
const auto part = name.substr(start, end - start);
|
||||
if (part.empty() || part == "." || part == "..") return false;
|
||||
if (!std::all_of(part.begin(), part.end(), [](char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
|
||||
}))
|
||||
return false;
|
||||
start = end + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool unpack(std::span<const unsigned char> bytes, const fs::path& target, std::string& issue) {
|
||||
struct Zip {
|
||||
mz_zip_archive value{};
|
||||
~Zip() { mz_zip_reader_end(&value); }
|
||||
} zip;
|
||||
if (!mz_zip_reader_init_mem(&zip.value, bytes.data(), bytes.size(), 0)) {
|
||||
issue = "the embedded pack is not a zip";
|
||||
return false;
|
||||
}
|
||||
const auto count = mz_zip_reader_get_num_files(&zip.value);
|
||||
std::uint64_t total{};
|
||||
for (mz_uint index = 0; index < count; ++index) {
|
||||
mz_zip_archive_file_stat info{};
|
||||
if (!mz_zip_reader_file_stat(&zip.value, index, &info) || info.m_is_encrypted) {
|
||||
issue = "the embedded pack has an unreadable entry";
|
||||
return false;
|
||||
}
|
||||
if (info.m_is_directory) continue;
|
||||
total += info.m_uncomp_size;
|
||||
if (!safe_entry(info.m_filename) || total > max_pack_bytes) {
|
||||
issue = std::string("the embedded pack has an entry it may not: ") + info.m_filename;
|
||||
return false;
|
||||
}
|
||||
const auto path = target / fs::path(info.m_filename);
|
||||
fs::create_directories(path.parent_path());
|
||||
struct File {
|
||||
HANDLE value;
|
||||
~File() { if (value != INVALID_HANDLE_VALUE) CloseHandle(value); }
|
||||
} file{CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
|
||||
struct Sink {
|
||||
HANDLE file;
|
||||
std::uint64_t written{}, limit;
|
||||
} sink{file.value, 0, info.m_uncomp_size};
|
||||
const auto write = [](void* opaque, mz_uint64 offset, const void* buffer, size_t size) -> size_t {
|
||||
auto& target = *static_cast<Sink*>(opaque);
|
||||
if (offset != target.written || size > target.limit - target.written) return 0;
|
||||
DWORD done{};
|
||||
if (!WriteFile(target.file, buffer, static_cast<DWORD>(size), &done, nullptr) || done != size) return 0;
|
||||
target.written += size;
|
||||
return size;
|
||||
};
|
||||
if (file.value == INVALID_HANDLE_VALUE || !mz_zip_reader_extract_to_callback(&zip.value, index, write, &sink, 0) ||
|
||||
sink.written != info.m_uncomp_size) {
|
||||
issue = std::string("could not unpack ") + info.m_filename;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<BuiltinPack> unpack_builtin_packs(const fs::path& mods_root, std::vector<std::string>& notes) noexcept {
|
||||
std::vector<BuiltinPack> packs;
|
||||
try {
|
||||
Source source;
|
||||
open_source(source);
|
||||
if (!source.module) return packs;
|
||||
for (const auto& pack : embedded) {
|
||||
const auto bytes = resource_bytes(source.module, pack.resource);
|
||||
if (bytes.empty()) continue;
|
||||
const auto directory = mods_root / std::string(builtin_folder) / pack.name;
|
||||
const auto version = version_of(bytes);
|
||||
std::error_code error;
|
||||
if (read_text(directory / version_file) != version) {
|
||||
// Unpacked beside it, then swapped in: a pack is never half there.
|
||||
const auto staging = fs::path(directory.wstring() + std::format(L".unpacking-{}", GetCurrentProcessId()));
|
||||
fs::remove_all(staging, error);
|
||||
std::string issue;
|
||||
if (!unpack(bytes, staging, issue)) {
|
||||
fs::remove_all(staging, error);
|
||||
notes.push_back(std::format("Built-in content {} could not be unpacked: {}", pack.name, issue));
|
||||
continue;
|
||||
}
|
||||
std::ofstream(staging / version_file, std::ios::binary) << version;
|
||||
fs::remove_all(directory, error);
|
||||
fs::rename(staging, directory, error);
|
||||
if (error) {
|
||||
fs::remove_all(staging, error);
|
||||
notes.push_back(std::format("Built-in content {} could not be put in place ({})", pack.name, error.message()));
|
||||
continue;
|
||||
}
|
||||
notes.push_back(std::format("Built-in content {} unpacked ({} bytes)", pack.name, bytes.size()));
|
||||
}
|
||||
packs.push_back({std::string("reskate-builtin-") + pack.name, directory});
|
||||
}
|
||||
} catch (const std::exception& failure) {
|
||||
notes.push_back(std::string("Built-in content could not be prepared: ") + failure.what());
|
||||
}
|
||||
return packs;
|
||||
}
|
||||
} // namespace dingosdk::mods
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Game content ReSkate carries itself: packs of game assets built by ReSkate Studio (the FPV
|
||||
// drone's model, tools/models/build_drone_pack.py), zipped into ReSkate.dll. Nobody installs or
|
||||
// sees them. Before a merge each is unpacked under Mods/.reskate-builtin/<name> (a dot folder,
|
||||
// which no mod list shows) when the one embedded differs from what is there, and the catalogue
|
||||
// merges it after every installed mod, always on (mod_catalog.cpp).
|
||||
namespace dingosdk::mods {
|
||||
inline constexpr std::string_view builtin_folder = ".reskate-builtin";
|
||||
struct BuiltinPack {
|
||||
std::string name; // as the merge knows it: reskate-builtin-<pack>
|
||||
std::filesystem::path directory; // Mods/.reskate-builtin/<pack>
|
||||
};
|
||||
// The packs ReSkate.dll carries, unpacked and current under `mods_root`. ReSkate.dll is the
|
||||
// module running this when it is loaded (the game); otherwise the copy beside this program (the
|
||||
// launcher) is opened as data. Never throws; `notes` says what was unpacked or went wrong.
|
||||
std::vector<BuiltinPack> unpack_builtin_packs(const std::filesystem::path& mods_root,
|
||||
std::vector<std::string>& notes) noexcept;
|
||||
} // namespace dingosdk::mods
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mod_catalog.h"
|
||||
#include "builtin_packs.h"
|
||||
|
||||
#include "mod_merge.h"
|
||||
#include "native_db.h"
|
||||
@@ -305,7 +306,12 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver
|
||||
result.issue = "The engine data root could not be resolved";
|
||||
return result;
|
||||
}
|
||||
// Content ReSkate carries itself (builtin_packs.h), unpacked first: it makes the Mods folder
|
||||
// when there is none, so the content loads without any mod installed.
|
||||
std::vector<std::string> builtin_notes;
|
||||
const auto builtin = unpack_builtin_packs(data_root / mods_folder, builtin_notes);
|
||||
auto list = scan_mods(data_root);
|
||||
for (auto& note : builtin_notes) list.notes.push_back(std::move(note));
|
||||
result.root = std::move(list.root);
|
||||
result.present = list.present;
|
||||
if (!result.present) return result;
|
||||
@@ -319,6 +325,9 @@ Catalog load_catalog(const std::filesystem::path& data_root, const MergeObserver
|
||||
try {
|
||||
for (const auto& name : list.missing)
|
||||
result.notes.push_back("mods.json names a folder that is not present: " + name);
|
||||
// After every installed mod, so none of theirs is ever shadowed by it; always on.
|
||||
for (const auto& pack : builtin)
|
||||
list.entries.push_back({describe_mod(pack.directory, pack.name, result.notes), true});
|
||||
// Board Break: a deck pack holds several decks in one mod folder (mod_catalog.h). Each deck
|
||||
// inside becomes a mod of its own for the run, listed after the pack as <folder>#<deck>, so
|
||||
// that a pack's decks are chosen, set aside and switched like decks in folders of their own.
|
||||
|
||||
+23
-18
@@ -323,6 +323,28 @@ bool valid_mod_name(std::string_view name) {
|
||||
});
|
||||
}
|
||||
|
||||
Mod describe_mod(const std::filesystem::path& directory, std::string name, std::vector<std::string>& notes) {
|
||||
std::error_code error;
|
||||
Mod mod;
|
||||
mod.name = std::move(name);
|
||||
mod.title = mod.name;
|
||||
mod.directory = directory;
|
||||
mod.provides_layout = fs::is_regular_file(mod.directory / layout_name, error);
|
||||
mod.provides_levels = fs::is_regular_file(mod.directory / levels_name, error);
|
||||
for (fs::directory_iterator it(mod.directory / "parks", error), end; !error && it != end; it.increment(error)) {
|
||||
auto file = ascii_path(it->path().filename());
|
||||
if (!file.ends_with(".park.json") || mod.park_maps.size() >= 16) continue;
|
||||
file.resize(file.size() - 10);
|
||||
mod.park_maps.push_back(std::move(file));
|
||||
}
|
||||
std::sort(mod.park_maps.begin(), mod.park_maps.end());
|
||||
read_build_info(mod, notes);
|
||||
read_mod_info(mod, notes);
|
||||
read_levels(mod);
|
||||
read_studio_marker(mod);
|
||||
return mod;
|
||||
}
|
||||
|
||||
ModList scan_mods(const std::filesystem::path& data_root) noexcept {
|
||||
ModList result;
|
||||
try {
|
||||
@@ -364,24 +386,7 @@ ModList scan_mods(const std::filesystem::path& data_root) noexcept {
|
||||
}
|
||||
ModEntry entry;
|
||||
entry.enabled = enabled;
|
||||
auto& mod = entry.mod;
|
||||
mod.name = found->second;
|
||||
mod.title = mod.name;
|
||||
mod.directory = result.root / mod.name;
|
||||
mod.provides_layout = fs::is_regular_file(mod.directory / layout_name, error);
|
||||
mod.provides_levels = fs::is_regular_file(mod.directory / levels_name, error);
|
||||
for (fs::directory_iterator it(mod.directory / "parks", error), end; !error && it != end; it.increment(error)) {
|
||||
auto file = ascii_path(it->path().filename());
|
||||
if (!file.ends_with(".park.json") || mod.park_maps.size() >= 16) continue;
|
||||
file.resize(file.size() - 10);
|
||||
mod.park_maps.push_back(std::move(file));
|
||||
}
|
||||
error.clear();
|
||||
std::sort(mod.park_maps.begin(), mod.park_maps.end());
|
||||
read_build_info(mod, result.notes);
|
||||
read_mod_info(mod, result.notes);
|
||||
read_levels(mod);
|
||||
read_studio_marker(mod);
|
||||
entry.mod = describe_mod(result.root / found->second, found->second, result.notes);
|
||||
result.entries.push_back(std::move(entry));
|
||||
}
|
||||
// Only mods named in the exclusions file are fingerprinted.
|
||||
|
||||
@@ -110,6 +110,9 @@ std::string ascii_path(const std::filesystem::path& path);
|
||||
// malformed, `issue` says why and `entries` still lists the folders (all
|
||||
// shown as enabled) so a manager can offer to rewrite it.
|
||||
ModList scan_mods(const std::filesystem::path& data_root) noexcept;
|
||||
// One mod folder as scan_mods reads it: its layout, levels, parks, info files and build stamp.
|
||||
// `name` is how it is known (for a folder under Mods/, the folder's name). Throws on I/O errors.
|
||||
Mod describe_mod(const std::filesystem::path& directory, std::string name, std::vector<std::string>& notes);
|
||||
|
||||
// Writes mods.json for `entries` in order, replacing the file atomically.
|
||||
// Throws with a readable message on failure.
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
#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 ®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<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
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
// Models placed in the world by ReSkate: an object blueprint loaded on the map, created as a
|
||||
// client-only entity and moved every frame (Engine/Game/Build/<build>/world_props.h).
|
||||
namespace dingosdk::multiplayer {
|
||||
// Other players' FPV drones. The drone's own model (made by ReSkate Studio from
|
||||
// assets/models/fpv/drone.glb) when it is loaded; until then a stand-in prop of the game's.
|
||||
inline constexpr std::string_view drone_model_blueprint = "reskate/drone/fpv_drone";
|
||||
inline constexpr std::string_view drone_model_stand_in = "world/props/common/construction/cmn_prp_trafficcone_01/cmn_prp_trafficcone_01";
|
||||
|
||||
// The client update thread, every frame while in a level: shows a model at each other player's
|
||||
// flying drone (drone_beacon.h), moves it, and takes it away when they land or leave. `show`
|
||||
// false (other players' drones hidden) takes them all away.
|
||||
void tick_drone_models(std::uintptr_t base, std::uintptr_t client, bool show) noexcept;
|
||||
std::string drone_models_status();
|
||||
|
||||
// The test that found how (dronemodeltest): spawns a named blueprint a few metres in front of the
|
||||
// local skater and moves it round a circle. Mode 0 the entity transform setter (what works),
|
||||
// 1 the blueprint transform then the setter, 2 the blueprint transform alone. Empty name: stop.
|
||||
inline constexpr std::string_view prop_test_default = drone_model_stand_in;
|
||||
void request_prop_test(std::string blueprint, int mode);
|
||||
void tick_prop_test(std::uintptr_t base, std::uintptr_t client) noexcept;
|
||||
std::string prop_test_status();
|
||||
// Where the test's prop is now, while it runs (client update thread): drone_sound.cpp plays a
|
||||
// motor there, so the sound can be heard without a second player.
|
||||
std::optional<std::array<float, 3>> prop_test_position() noexcept;
|
||||
|
||||
// The engine's entity destruction hook (native_skater.cpp): a prop the engine destroys (a map
|
||||
// change takes them all) is forgotten before it is freed. Any thread; cheap with none placed.
|
||||
void prop_entity_destroyed(std::uintptr_t entity) noexcept;
|
||||
} // namespace dingosdk::multiplayer
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "native_skater_internal.h"
|
||||
#include "native_prop.h"
|
||||
#include "native_pose_layout.h"
|
||||
#include "puppet_cost.h"
|
||||
#include "Extension/Multiplayer/Session/monotonic_clock.h"
|
||||
@@ -385,6 +386,7 @@ void animation_hook(std::uintptr_t component, std::uintptr_t update) {
|
||||
void destroy_hook(std::uintptr_t entity, std::uintptr_t owner) {
|
||||
// Other SDK-created actors share this hook; they must forget the entity
|
||||
// before the engine frees it.
|
||||
prop_entity_destroyed(entity);
|
||||
if (const auto listener = shared().destroyed_listener.load(std::memory_order_acquire))
|
||||
listener(entity);
|
||||
// The first slot watching it as either skater or board.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ai_skaters.h"
|
||||
#include "drone_flight.h"
|
||||
#include "drone_outfit.h"
|
||||
#include "Extension/Multiplayer/Remote/native_prop.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <format>
|
||||
@@ -175,6 +176,34 @@ void register_movement_commands(Commands ®istry) {
|
||||
profile_runtime::set_local_values({{"Drone.Radio", 1.0}});
|
||||
};
|
||||
registry.add(std::move(radio_mode));
|
||||
// A test toward the drone's 3D model (Multiplayer/Remote/native_prop.h): can a spawned prop
|
||||
// be moved every frame?
|
||||
auto model_name = argument("blueprint", Type::text, true);
|
||||
auto model_mode = argument("mode", Type::integer, true);
|
||||
auto model_test = action("dronemodeltest",
|
||||
"Test for the drone model: spawns the drone (dronemodeltest drone), the traffic cone (cone) or a named blueprint in front of you and moves it "
|
||||
"round a circle. Mode 0, 1 or 2 tries a different way of moving it. 'dronemodeltest stop' removes it; on its own it reports",
|
||||
Group::movement, {model_name, model_mode});
|
||||
model_test.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
|
||||
model_test.run = [](const Model &, const Values &args, const Output &out) {
|
||||
const auto name = args.empty() ? std::string() : std::get<std::string>(args[0]);
|
||||
const int mode = args.size() > 1 ? static_cast<int>(std::get<std::int64_t>(args[1])) : 1;
|
||||
if (name.empty() || name == "status") {
|
||||
out(multiplayer::prop_test_status());
|
||||
out("Other players' drones: " + multiplayer::drone_models_status());
|
||||
return;
|
||||
}
|
||||
if (name == "stop") {
|
||||
multiplayer::request_prop_test({}, 0);
|
||||
out("Removing the test prop.");
|
||||
return;
|
||||
}
|
||||
const std::string blueprint = name == "drone" ? std::string(multiplayer::drone_model_blueprint)
|
||||
: name == "cone" || name == "start" ? std::string(multiplayer::prop_test_default) : name;
|
||||
multiplayer::request_prop_test(blueprint, mode);
|
||||
out(std::format("Spawning {} with mode {}. Run dronemodeltest again in a moment for what happened.", blueprint, mode));
|
||||
};
|
||||
registry.add(std::move(model_test));
|
||||
auto radio_check = action("droneradiocheck", "Show what your controller is sending right now, and how the drone reads it", Group::movement);
|
||||
radio_check.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
|
||||
radio_check.run = [](const Model &, const Values &, const Output &out) {
|
||||
|
||||
@@ -522,6 +522,7 @@ void lobby_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
|
||||
toggle(menu, callbacks, Knob::kBeacon, "Let other players see my drone");
|
||||
toggle(menu, callbacks, Knob::kShowBeacons, "Show other players' drones");
|
||||
note("Other players only see drones if they are on a version of ReSkate with the drone.");
|
||||
note("Drone model: FPV Drone by PinaPipe (CC BY 4.0), reduced and rescaled for the game.");
|
||||
end_card();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "Extension/UI/NativeMenu/native_menu.h"
|
||||
#include "Extension/Multiplayer/Hud/native_party.h"
|
||||
#include "Extension/Multiplayer/Session/session.h"
|
||||
#include "Extension/Multiplayer/Remote/native_prop.h"
|
||||
#include "Extension/Boot/discord_presence.h"
|
||||
#include "Engine/Game/World/world_names.h"
|
||||
#include "Extension/Multiplayer/Hud/custom_nametags.h"
|
||||
@@ -1284,6 +1285,12 @@ void tick(std::uintptr_t client, std::uintptr_t update) {
|
||||
{
|
||||
DINGO_PROFILE_ZONE("tick/multiplayer");
|
||||
dingosdk::multiplayer::tick(r.base,client,multiplayer_ready,r.multiplayer_map,load_multiplayer_map);
|
||||
if (multiplayer_ready) {
|
||||
dingosdk::multiplayer::tick_prop_test(r.base, client);
|
||||
// Other players' drones as models in the world (DRONE page, LOBBY tab: show other players' drones).
|
||||
dingosdk::multiplayer::tick_drone_models(r.base, client,
|
||||
dingosdk::drone::knobs().value[dingosdk::drone::kShowBeacons].load(std::memory_order_relaxed) >= 0.5f);
|
||||
}
|
||||
if (auto notice = dingosdk::multiplayer::take_leave_notice(); !notice.empty())
|
||||
dingosdk::overlay::notify(dingosdk::overlay::NoticeLevel::warning, "Map not installed", std::move(notice));
|
||||
// Kicked, banned or otherwise put out by the host or server: said in the middle of
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
# FPV drone models
|
||||
|
||||
Both are licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), which allows them to
|
||||
be shipped, changed and shared as long as the author is credited and changes are noted.
|
||||
|
||||
| File | Title | Author | Source |
|
||||
| --- | --- | --- | --- |
|
||||
| `fpv_drone.glb` | FPV Drone | PinaPipe (https://sketchfab.com/PinaPipe) | https://sketchfab.com/3d-models/fpv-drone-92152c31ec9a4a87b2a0478bd114d293 |
|
||||
| `dji_avata2.glb` | DJI Avata2 | raphael.harris.gaffga (https://sketchfab.com/raphael.harris.gaffga) | https://sketchfab.com/3d-models/dji-avata2-e27ed758e2174a89a48368e84027f8d9 |
|
||||
|
||||
`drone.glb` is the game model, made from `fpv_drone.glb` by `tools/models/prepare_drone.py`:
|
||||
reduced from 27,946 to about 8,000 triangles, textures shrunk to 512 pixels, scaled to 0.29 m
|
||||
across, centred and turned to face forward:
|
||||
|
||||
blender --background --python tools/models/prepare_drone.py -- assets/models/fpv/fpv_drone.glb assets/models/fpv/drone.glb --triangles 8000 --span 0.33 --forward 0.305,-0.952 --texture-size 512
|
||||
|
||||
In game the credit reads: "Drone model: FPV Drone by PinaPipe (CC BY 4.0)".
|
||||
Binary file not shown.
+3
-2
@@ -105,8 +105,9 @@ if(WIN32)
|
||||
Engine/Vfs/mod_merge_load_screens.cpp
|
||||
Engine/Vfs/mod_music.cpp
|
||||
Engine/Vfs/mod_scoring.cpp
|
||||
Engine/Vfs/mod_store_copies.cpp)
|
||||
target_link_libraries(dingosdk_mods PUBLIC dingosdk_json dingosdk_mod_list PRIVATE dingosdk_game_archives dingosdk_native_db dingosdk_frostbite dingosdk_content_cache shell32 bcrypt)
|
||||
Engine/Vfs/mod_store_copies.cpp
|
||||
Engine/Vfs/builtin_packs.cpp)
|
||||
target_link_libraries(dingosdk_mods PUBLIC dingosdk_json dingosdk_mod_list PRIVATE dingosdk_game_archives dingosdk_native_db dingosdk_frostbite dingosdk_content_cache dingosdk_miniz shell32 bcrypt)
|
||||
|
||||
add_library(dingosdk_custom_scripts STATIC Engine/Scripting/custom_scripts.cpp)
|
||||
target_link_libraries(dingosdk_custom_scripts PUBLIC dingosdk_json PRIVATE dingosdk_initfs)
|
||||
|
||||
@@ -32,6 +32,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/UI/NativeMenu/native_menu_data.cpp
|
||||
Extension/Multiplayer/Remote/native_skater.cpp
|
||||
Extension/Multiplayer/Remote/native_skater_spawn.cpp
|
||||
Extension/Multiplayer/Remote/native_prop.cpp
|
||||
Extension/Multiplayer/Remote/puppet_cost.cpp
|
||||
Extension/Multiplayer/Remote/native_cosmetics.cpp
|
||||
Extension/Multiplayer/Remote/native_audio.cpp
|
||||
@@ -315,6 +316,17 @@ if(EXISTS "${dingosdk_emotes}/emotes.json" AND EXISTS "${dingosdk_emotes}/emotes
|
||||
"EMOTES_PNG RCDATA \"${dingosdk_emotes_native}\\\\emotes.png\"\n")
|
||||
target_sources(dingosdk_runtime PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/emotes.rc")
|
||||
endif()
|
||||
# Game content ReSkate carries itself (Engine/Vfs/builtin_packs.h): the FPV drone's model, built
|
||||
# by tools/models/build_drone_pack.py. Without the zip, other players' drones use a stand-in prop.
|
||||
set(dingosdk_drone_pack "${PROJECT_SOURCE_DIR}/assets/builtin/fpv_drone.zip")
|
||||
if(EXISTS "${dingosdk_drone_pack}")
|
||||
file(TO_NATIVE_PATH "${dingosdk_drone_pack}" dingosdk_drone_pack_native)
|
||||
string(REPLACE "\\" "\\\\" dingosdk_drone_pack_native "${dingosdk_drone_pack_native}")
|
||||
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/generated/builtin_packs.rc"
|
||||
"BUILTIN_PACK_FPV_DRONE RCDATA \"${dingosdk_drone_pack_native}\"\n")
|
||||
target_sources(dingosdk_runtime PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/builtin_packs.rc")
|
||||
set_property(SOURCE "${CMAKE_CURRENT_BINARY_DIR}/generated/builtin_packs.rc" APPEND PROPERTY OBJECT_DEPENDS "${dingosdk_drone_pack}")
|
||||
endif()
|
||||
# The music sources' setup page (Extension/Music/spotify_dashboard.h).
|
||||
file(TO_NATIVE_PATH "${PROJECT_SOURCE_DIR}/Extension/Music/setup_page.html" dingosdk_music_page_native)
|
||||
string(REPLACE "\\" "\\\\" dingosdk_music_page_native "${dingosdk_music_page_native}")
|
||||
|
||||
Reference in new issue
Block a user