diff --git a/Engine/Game/Build/20260929/native_cosmetics.h b/Engine/Game/Build/20260929/native_cosmetics.h index e56c2af0..97f06930 100644 --- a/Engine/Game/Build/20260929/native_cosmetics.h +++ b/Engine/Game/Build/20260929/native_cosmetics.h @@ -17,6 +17,15 @@ inline constexpr std::uintptr_t publish_materials = 0x1331420; inline constexpr std::array publish_materials_prefix{ 0x48,0x89,0x5c,0x24,0x08,0x89,0x54,0x24,0x10,0x55,0x56,0x57,0x41,0x54,0x41,0x55, 0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x20}; +// Typed per-instance shader setter reached by MaterialInstance:addEsVector. +// (material, ParamDbKey*, NativeValue*, priority) -> success. Inserts with the +// engine allocator and preserves the existing priority rules. +inline constexpr std::uintptr_t set_shader_parameter = 0x1312d00; +inline constexpr std::array set_shader_parameter_prefix{ + 0x48,0x89,0x5c,0x24,0x20,0x48,0x89,0x4c,0x24,0x08,0x55,0x56,0x57,0x41,0x54,0x41, + 0x55,0x48,0x8d,0x6c,0x24,0xc9,0x48,0x81}; +// Initialized ColorRgb type descriptor; validate its type hash, size and index. +inline constexpr std::uintptr_t color_rgb_type = 0x7780c50; // Finds a named entry in a loaded table by the FNV-1a hash of its name: // (out, name, table, flag, context) -> out, the fifth passed on the stack. // The skater loader's preset lookups reach it through a Lua binding that diff --git a/Engine/Vfs/world_layer_scan.cpp b/Engine/Vfs/world_layer_scan.cpp index dd369c2d..790a40a4 100644 --- a/Engine/Vfs/world_layer_scan.cpp +++ b/Engine/Vfs/world_layer_scan.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace dingosdk::world_layer_scan { namespace fs = std::filesystem; @@ -211,12 +212,23 @@ WorldLayerCatalog from_json(const Json& document) { WorldLayerCatalog scan(const fs::path& gameRoot) { struct Result { std::vector references; std::set shipped; }; std::vector> pending; - for (const auto& source : maps) - pending.push_back(std::async(std::launch::async, [&gameRoot, toc = source.toc] { + std::vector workers; + pending.reserve(maps.size()); + workers.reserve(maps.size()); + // MSVC std::async schedules onto Windows' existing thread pool. During + // pre-entrypoint initialization the launcher gates those existing threads, + // so waiting for a pool job deadlocks on a cold/stale layer cache. Dedicated + // threads are created after the gate and can finish the scan independently. + // jthread also joins all started jobs if creation or a scan throws. + for (const auto& source : maps) { + std::packaged_task task([&gameRoot, toc = source.toc] { Result result; result.references = read_references(gameRoot, toc, result.shipped); return result; - })); + }); + pending.push_back(task.get_future()); + workers.emplace_back(std::move(task)); + } WorldLayerCatalog catalog; for (std::size_t i = 0; i < maps.size(); ++i) { const auto result = pending[i].get(); diff --git a/Extension/Customization/developer_board.cpp b/Extension/Customization/developer_board.cpp new file mode 100644 index 00000000..0df1cbf1 --- /dev/null +++ b/Extension/Customization/developer_board.cpp @@ -0,0 +1,95 @@ +#include "developer_board.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Core/Log/logging.h" +#include "Extension/Multiplayer/Steam/steam_social.h" +#include "Extension/Multiplayer/Remote/native_skater.h" +#include +#include + +namespace dingosdk { +namespace { +void report_board(std::uintptr_t entity, std::uint64_t steam_id, std::string_view message, logging::Level level) noexcept { + struct Report { std::uintptr_t entity{}; std::uint64_t at{}; std::uint32_t message{}; }; + static std::array reports{}; + const auto now = GetTickCount64(); + const auto hash = developer_board_detail::material_hash(message); + auto *entry = &reports.front(); + for (auto &candidate : reports) { + if (candidate.entity == entity) { entry = &candidate; break; } + if (candidate.at < entry->at) entry = &candidate; + } + if (entry->entity == entity && (entry->message == hash || now - entry->at < 5000)) return; + *entry = {entity, now, hash}; + logging::log(level, logging::Channel::customization, "Developer board (Steam {}, actor {:#x}): {}", steam_id, entity, message); +} +bool write_color(std::uintptr_t base, const developer_board_detail::Binding &binding, const developer_board_detail::Color &color) noexcept { + __try { + if (!binding.node) { + const developer_board_detail::NativeColor value{color, 0, binding.type}; + // Let the engine allocate/copy/hash a missing typed parameter. Priority + // zero allows later native appearance presets to replace this override. + return reinterpret_cast( + base + addr::native_cosmetics::set_shader_parameter)(binding.material, &binding.key, &value, 0) != 0; + } + std::memcpy(reinterpret_cast(binding.node + 0x10), color.data(), sizeof(color)); + *reinterpret_cast(binding.material + 0x2f6) = 1; + return true; + } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } +} +bool publish_materials(std::uintptr_t base, std::uintptr_t item) noexcept { + __try { + reinterpret_cast(base + addr::native_cosmetics::publish_materials)(item, 0); + return true; + } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } +} +} // namespace +void update_developer_board(std::uintptr_t base, std::uintptr_t board, std::uint64_t steam_id, + std::uint64_t generation, DeveloperBoardState &state) noexcept { + const bool developer = multiplayer::reskate_developer(steam_id); + if (!board) { state = {}; return; } + if (!developer && !state.count) return; + try { + auto read = [](std::uintptr_t address, void *out, std::size_t size) { return memory::peek_bytes(address, out, size); }; + std::array publisher{}; + std::array setter{}; + if (!read(base + addr::native_cosmetics::publish_materials, publisher.data(), publisher.size()) || + publisher != addr::native_cosmetics::publish_materials_prefix || + !read(base + addr::native_cosmetics::set_shader_parameter, setter.data(), setter.size()) || + setter != addr::native_cosmetics::set_shader_parameter_prefix) { + report_board(board, steam_id, "material functions differ from the supported build", logging::Level::warning); + return; + } + const auto live = developer_board_detail::materials(read, base, board); + bool published = true; + auto write = [base](const auto &binding, const auto &color) { return write_color(base, binding, color); }; + auto publish = [base, &published](std::uintptr_t item) { if (!publish_materials(base, item)) published = false; }; + developer_board_detail::animate(state, live, board, generation, developer, GetTickCount64(), write, publish); + const auto wanted = static_cast(std::count_if(live.bindings.begin(), live.bindings.begin() + live.count, + [](const auto &binding) { return binding.eligible; })); + if (!published) report_board(board, steam_id, "native material publication failed", logging::Level::warning); + else if (developer && !live.pending && state.count < wanted) + report_board(board, steam_id, "waiting for the remaining board color overrides", logging::Level::warning); + else if (developer && state.count) + report_board(board, steam_id, "RGB active on the owned pink board cosmetics", logging::Level::info); + } catch (const std::exception &error) { + report_board(board, steam_id, error.what(), logging::Level::warning); + } catch (...) {} +} +void tick_local_developer_board(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept { + static DeveloperBoardState state; + if (!ready) return; + try { + const auto social = multiplayer::steam_social_snapshot(); + const auto id = social ? social->local.id : 0; + if (!multiplayer::reskate_developer(id) && !state.count) return; + const auto local = multiplayer::capture_local(base, client, false); + if (!local.ready) return; + auto read = [](std::uintptr_t address, void *out, std::size_t size) { return memory::peek_bytes(address, out, size); }; + // Cosmetic changes can replace the board while retaining the skater. + // Re-resolve the actual local association, rather than capture's cached board. + const auto board = multiplayer::read_native_board(read, base, local.entity); + update_developer_board(base, board.entity, id, local.context, state); + } catch (...) {} +} +} // namespace dingosdk diff --git a/Extension/Customization/developer_board.h b/Extension/Customization/developer_board.h new file mode 100644 index 00000000..678eb74a --- /dev/null +++ b/Extension/Customization/developer_board.h @@ -0,0 +1,8 @@ +#pragma once +#include "developer_board_material.h" + +namespace dingosdk { +void update_developer_board(std::uintptr_t base, std::uintptr_t board, std::uint64_t steam_id, + std::uint64_t generation, DeveloperBoardState &state) noexcept; +void tick_local_developer_board(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept; +} // namespace dingosdk diff --git a/Extension/Customization/developer_board_material.h b/Extension/Customization/developer_board_material.h new file mode 100644 index 00000000..1b432b9c --- /dev/null +++ b/Extension/Customization/developer_board_material.h @@ -0,0 +1,213 @@ +#pragma once +#include "developer_hoodie_material.h" +#include + +namespace dingosdk { +namespace developer_board_detail { +using developer_hoodie_detail::Color; +using developer_hoodie_detail::material_hash; +using developer_hoodie_detail::rainbow; +inline constexpr std::uint64_t graphic_color = 0x873f48af36e50d59ULL; +inline constexpr std::uint64_t base_color = 0x8c46cd354b18a9e2ULL; +inline constexpr Color shader_default{.5f, .5f, .5f}; +inline constexpr std::array slots{ + "Characters/Customization/Board/Slots/Skateboard_Deck_Slot", + "Characters/Customization/Board/Slots/Skateboard_Trucks_Slot", + "Characters/Customization/Board/Slots/Skateboard_Wheels_Slot"}; +struct Target { + std::string_view asset, material; + std::uint32_t recipe_slot; + std::size_t controller; + std::uint64_t key; +}; +// The board loader registers Deck_Mat / DeckTop_Mat for the compiled sections +// Skateboard_base_mat / mat_skateboard_top. Use the live named map identities. +// Recipe identities, assigned board controllers and serialized shader keys from +// the four pink cosmetics. The deck's wood follows its Solid Pink graphic. +inline constexpr std::array targets{{ + {"Own_DeckGraphic_Gen_Popsicle_00021", "Deck_Mat", 0x66071628, 0, graphic_color}, + {"Own_DeckGraphic_Gen_Popsicle_00021", "Deck_Mat", 0x66071628, 0, base_color}, + {"Own_DeckGripColor_Gen_Popsicle_00006", "DeckTop_Mat", 0x755d5ee0, 0, graphic_color}, + {"Own_Truck_Gen_Default_00002", "Truck_Mat", 0x0add98be, 1, base_color}, + {"Own_WheelColor_Gen_Classic_00005", "Wheel_mat", 0xc83bb94e, 2, base_color}, +}}; +// Native ParamDbKey and inline NativeValue layout used by addEsVector's setter. +// The key's first word is the final two serialized GUID bytes. Its type index +// comes from the live ColorRgb descriptor, not a serialized Vec3 or boxed value. +struct ParameterKey { + std::uint16_t identity{}, size{}, type{}, count{}; + std::uint64_t hash{}; +}; +struct NativeColor { + Color color{}; + std::uint32_t padding{}; + std::uintptr_t type{}; + std::uint32_t flags{3}, reserved{}; +}; +static_assert(sizeof(ParameterKey) == 16 && offsetof(ParameterKey, hash) == 8); +static_assert(sizeof(NativeColor) == 32 && offsetof(NativeColor, type) == 16 && offsetof(NativeColor, flags) == 24); +inline ParameterKey parameter_key(std::uint64_t hash, std::uint16_t type) noexcept { + return {static_cast(hash == graphic_color ? 0xd442 : 0x8c54), sizeof(Color), type, 1, hash}; +} +struct Binding { + std::uintptr_t item{}, material{}, node{}, type{}; + ParameterKey key{}; + Color color{}; + bool eligible{}; +}; +struct SavedColor { + Binding binding; + Color original{}, last{}; +}; +inline constexpr std::size_t max_bindings = targets.size() * 4; +struct Materials { + std::uintptr_t component{}, appearance{}; + std::array bindings{}; + std::size_t count{}; + bool pending{}; +}; +template +std::uintptr_t parameter(Memory &m, std::uintptr_t material, std::uint64_t key) { + const auto buckets = m.ptr(material, 0x268); + const auto size = m.template get(material, 0x270); + const auto total = m.template get(material, 0x274); + // The native setter also uses these buckets when inserting a missing color. + m.check(buckets && size && size <= 1024 && total <= 512, "Board parameter map exceeds bounds."); + const auto sentinel = m.ptr(buckets, size * 8ULL); + auto node = m.ptr(buckets, (key % size) * 8ULL); + std::uint32_t visited{}; + for (; node && node != sentinel && visited < total; ++visited, node = m.ptr(node, 0x40)) + if (m.template get(node, 8) == key) return node; + m.check(!node || node == sentinel, "Board parameter chain exceeds bounds."); + return 0; +} +template Materials materials(Read &read, std::uintptr_t base, std::uintptr_t entity) { + using namespace multiplayer; + CosmeticMemory m{read, base}; + Materials out; + m.check(m.ptr(entity) == base + addr::engine::board_entity_vtable, "RGB board actor type differs."); + out.component = m.component(entity); + out.pending = m.template get(out.component, 0x10a) || m.template get(out.component, 0x10d); + const auto recipe = m.ptr(out.component, 0x158); + const auto count = m.count(recipe, sizeof(NativeCosmeticItem), max_cosmetic_slots); + std::array eligible{}; + for (std::size_t i = 0; i < count; ++i) { + const auto item = m.template get(recipe, i * sizeof(NativeCosmeticItem)); + for (std::size_t t = 0; t < targets.size(); ++t) { + const auto &target = targets[t]; + if (item.slot != target.recipe_slot || item.hash != cosmetic_asset_hash(target.asset)) continue; + const auto name = item.asset ? m.text(reinterpret_cast(item.asset)) : std::string{}; + eligible[t] = name.empty() || name == target.asset; + } + } + const auto type = base + addr::native_cosmetics::color_rgb_type; + const auto info = m.ptr(type); + m.check(info >= base && info < base + 0x09144000 && m.template get(info) == 0x885eff59 && + m.template get(info, 6) == sizeof(Color), "Board ColorRgb descriptor differs."); + const auto type_index = m.template get(type, 8); + out.appearance = read_native_component(read, entity, base + addr::engine::skater_appearance_vtable); + const auto begin = m.ptr(out.appearance, 0x90), end = m.ptr(out.appearance, 0x98), capacity = m.ptr(out.appearance, 0xa0); + m.check(begin <= end && end <= capacity && (end - begin) % 8 == 0 && (capacity - begin) % 8 == 0 && + (capacity - begin) / 8 <= 64 && (begin || !capacity), "Board controller array exceeds bounds."); + std::array seen{}; + for (auto entry = begin; entry < end; entry += 8) { + const auto item = m.ptr(entry); + if (!item) continue; + m.check(m.ptr(item, 0x48) == out.appearance + 0x40, "Board controller ownership differs."); + const auto name = m.text(m.ptr(item, 0x30)); + const auto assigned = std::find(slots.begin(), slots.end(), name); + if (assigned == slots.end()) continue; + const auto slot = static_cast(assigned - slots.begin()); + m.check(!seen[slot], "Board has duplicate slot controllers."); + seen[slot] = true; + if (!(m.template get(item, 0xb0) & 2)) { out.pending = true; continue; } + const auto buckets = m.ptr(item, 0x1f0); + const auto size = m.template get(item, 0x1f8), total = m.template get(item, 0x1fc); + if (!total) { out.pending = true; continue; } + m.check(buckets && size && size <= 256 && total <= 128, "Board material map exceeds bounds."); + const auto sentinel = m.ptr(buckets, size * 8ULL); + for (std::size_t t = 0; t < targets.size(); ++t) { + const auto &target = targets[t]; + if (target.controller != slot) continue; + const auto hash = material_hash(target.material); + auto node = m.ptr(buckets, (hash % size) * 8ULL); + std::uint32_t visited{}; + for (; node && node != sentinel && visited < total; ++visited, node = m.ptr(node, 0x28)) + if (m.template get(node) == hash) break; + m.check(!node || node == sentinel || visited < total, "Board material chain exceeds bounds."); + if (!node || node == sentinel) continue; + std::array variants{}; + for (std::size_t v = 0; v < variants.size(); ++v) { + const auto material = m.ptr(node, 8 + v * 8); + if (!material || std::find(variants.begin(), variants.begin() + v, material) != variants.begin() + v) continue; + variants[v] = material; + const auto p = parameter(m, material, target.key); + const auto key = parameter_key(target.key, type_index); + Color color = shader_default; + if (p) { + const auto native_key = m.template get(p); + m.check(native_key.identity == key.identity && native_key.size == key.size && native_key.type == key.type && + native_key.count == key.count && m.ptr(p, 0x20) == type && + (m.template get(p, 0x28) & 1), "Board color parameter type differs."); + color = m.template get(p, 0x10); + m.check(std::all_of(color.begin(), color.end(), [](float c) { return std::isfinite(c); }), "Board color is invalid."); + } + m.check(out.count < out.bindings.size(), "Board colors exceed bounds."); + out.bindings[out.count++] = {item, material, p, type, key, color, eligible[t]}; + } + } + } + return out; +} +inline bool same_binding(const Binding &live, const Binding &saved) noexcept { + // A successful native insertion acquires its node on the next fresh walk. + return live.item == saved.item && live.material == saved.material && live.type == saved.type && live.key.hash == saved.key.hash && + (saved.node ? live.node == saved.node : live.node != 0); +} +} // namespace developer_board_detail +struct DeveloperBoardState { + std::uintptr_t entity{}, component{}, appearance{}; + std::uint64_t generation{}; + std::array colors{}; + std::size_t count{}; +}; +namespace developer_board_detail { +// Re-discover ownership before every write. Changing the recipe restores only +// colors that still equal our last write; native preset updates take precedence. +// Missing keys inherit .5 from these compiled board shaders. Keep inserted +// overrides at native preset priority and restore that default on deactivation. +// Never clear a NativeValue: the renderer still copies the key's full payload. +template +void animate(DeveloperBoardState &state, const Materials &live, std::uintptr_t entity, std::uint64_t generation, + bool developer, std::uint64_t milliseconds, Write &write, Publish &publish) { + if (state.entity != entity || state.generation != generation || state.component != live.component || state.appearance != live.appearance) + state = {}; + if (live.pending) return; + DeveloperBoardState next; + next.entity = entity; next.generation = generation; next.component = live.component; next.appearance = live.appearance; + const auto color = rainbow(milliseconds); + std::array changed_items{}; + std::size_t changed{}; + for (std::size_t i = 0; i < live.count; ++i) { + const auto &binding = live.bindings[i]; + const SavedColor *saved{}; + for (std::size_t j = 0; j < state.count; ++j) + if (same_binding(binding, state.colors[j].binding)) { saved = &state.colors[j]; break; } + bool wrote{}; + if (developer && binding.eligible) { + const auto original = saved && binding.color == saved->last ? saved->original : binding.color; + if ((binding.node && binding.color == color) || (wrote = write(binding, color))) + next.colors[next.count++] = {binding, original, color}; + else if (saved) next.colors[next.count++] = *saved; + } else if (saved && binding.color == saved->last && binding.color != saved->original) { + wrote = write(binding, saved->original); + if (!wrote) next.colors[next.count++] = *saved; + } + if (wrote && std::find(changed_items.begin(), changed_items.begin() + changed, binding.item) == changed_items.begin() + changed) + changed_items[changed++] = binding.item; + } + state = next; + for (std::size_t i = 0; i < changed; ++i) publish(changed_items[i]); +} +} // namespace developer_board_detail +} // namespace dingosdk diff --git a/Extension/Multiplayer/Remote/native_skater.cpp b/Extension/Multiplayer/Remote/native_skater.cpp index c90a2c8e..10362bdb 100644 --- a/Extension/Multiplayer/Remote/native_skater.cpp +++ b/Extension/Multiplayer/Remote/native_skater.cpp @@ -565,6 +565,9 @@ NativeAnimationStats remote_animation_stats() noexcept { std::uintptr_t remote_skater_entity() noexcept { return watched().entity[peer_slot].load(std::memory_order_acquire); } +std::uintptr_t remote_board_entity() noexcept { + return watched().board[peer_slot].load(std::memory_order_acquire); +} std::uint64_t remote_skater_generation() noexcept { return remote().generation.load(std::memory_order_acquire); } diff --git a/Extension/Multiplayer/Remote/native_skater.h b/Extension/Multiplayer/Remote/native_skater.h index 742e4de0..94fbf3f0 100644 --- a/Extension/Multiplayer/Remote/native_skater.h +++ b/Extension/Multiplayer/Remote/native_skater.h @@ -25,6 +25,7 @@ std::uint64_t remote_pose_updates() noexcept; std::uint64_t remote_board_pose_updates() noexcept; NativeAnimationStats remote_animation_stats() noexcept; std::uintptr_t remote_skater_entity() noexcept; +std::uintptr_t remote_board_entity() noexcept; // Changes whenever the current slot's actor is created, removed or destroyed. std::uint64_t remote_skater_generation() noexcept; // Client thread. Installs the shared animation/destruction hooks without diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 3a85b712..ada649d1 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -445,6 +445,7 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui if (was_visible && local.ready && !p.render_failed && p.far_interval && now < p.next_far_sample && !(p.far_for_view && !out_of_view(p, view))) { update_developer_hoodie(s.base, remote_skater_entity(), p.member.id, remote_skater_generation(), p.developer_hoodie); + update_developer_board(s.base, remote_board_entity(), p.member.id, remote_skater_generation(), p.developer_board); present_audio(p); update_party_position(&p.render_pose); if (labels) label(p); @@ -521,6 +522,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui } update_developer_hoodie(s.base, p.visible ? remote_skater_entity() : 0, p.member.id, remote_skater_generation(), p.developer_hoodie); + update_developer_board(s.base, p.visible ? remote_board_entity() : 0, p.member.id, + remote_skater_generation(), p.developer_board); if (!p.visible) { stop_remote_audio(); p.presented_audio.reset(); diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 0ac4d32b..75e1c27e 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -2,6 +2,7 @@ #include "Engine/Game/Multiplayer/chat_rate.h" #include "session.h" #include "Extension/Customization/developer_hoodie.h" +#include "Extension/Customization/developer_board.h" #include "Extension/Multiplayer/Remote/native_skater.h" #include "Extension/Multiplayer/Voice/voice_chat.h" #include "Extension/Multiplayer/Steam/steam_transport.h" @@ -98,6 +99,7 @@ struct Peer { // (0 after a spawn) to spread native recipe applies over frames. std::uint64_t cosmetic_revision{}, applied_cosmetics{}; DeveloperHoodieState developer_hoodie; + DeveloperBoardState developer_board; AudioBuffer audio; std::uint32_t voice_sequence{}; bool received_voice{}; diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index af780d64..2df1c6b3 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -1,5 +1,6 @@ #include "runtime_internal.h" #include "Extension/Customization/developer_hoodie.h" +#include "Extension/Customization/developer_board.h" #include "Extension/Assets/live_mods.h" #include "Engine/Core/Log/logging.h" #include "Engine/Core/Profiling/profiler.h" @@ -985,6 +986,7 @@ void tick(std::uintptr_t client, std::uintptr_t update) { dingosdk::multiplayer::tick(r.base,client,multiplayer_ready,r.multiplayer_map,load_multiplayer_map); } dingosdk::tick_local_developer_hoodie(r.base, client, multiplayer_ready); + dingosdk::tick_local_developer_board(r.base, client, multiplayer_ready); // The session spawns and places skaters and can teleport: check the camera again. tick_state.camera_issue.reset(); // The spectate camera follows a moving skater, so it runs every client tick, after the diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index c8da6ebc..eb4a52cc 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -93,6 +93,7 @@ add_library(dingosdk_runtime SHARED Extension/World/loading_screen.cpp Extension/Customization/preset_lookup_guard.cpp Extension/Customization/developer_hoodie.cpp + Extension/Customization/developer_board.cpp Extension/Skater/skater_model.cpp Extension/Skater/skater_observer.cpp Extension/Skater/client_source_spawn.cpp