mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-28 02:27:07 +01:00
tag preview, separate switches and per-item colors
- Split the single switch into "Show my tag" and "Show my items", and show the tag as chat and nametags draw it. - Color whatever is equipped instead of the one hoodie and pink board: top, bottoms, shoes, socks, hat, glasses, outfit, costume, deck, grip tape, trucks and wheels, each set on its own. - Each item can use its list's animation (rainbow, red or gold), a gradient between two picked colors, one solid color, or be off. - The choices travel with the player's appearance so everyone sees them. Protocol version 41.
This commit is contained in:
1 parent
c930b512e6
commit
5bfe4f7bfa
20 files changed
+910
-421
No files matched your search
@@ -114,9 +114,23 @@ struct MultiplayerModel {
|
||||
// Local: bad words in chat names and messages show as **** (on by default).
|
||||
bool chat_filter{true};
|
||||
// Local: the tag the ReSkate backend gives this player ("Dev", "Creator" or "Homie"; empty
|
||||
// for most players), and whether they show it and its animated items, to everyone.
|
||||
// for most players) and its role colour, and whether they show it, and the animated items
|
||||
// that come with it, to everyone.
|
||||
std::string identity_tag;
|
||||
bool identity_marks{true};
|
||||
std::uint32_t identity_tag_colour{};
|
||||
bool identity_tag_shown{true}, identity_items_shown{true};
|
||||
// Local: how each of this player's marked cosmetics is coloured, for the Special page: what
|
||||
// they wear in each slot ("Top", "Shoes", ...), then the parts of their board. mode: 0 what
|
||||
// their list gives (`identity_animation`: "RAINBOW", "RED" or "GOLD"), 1 off, 2 a gradient
|
||||
// between the two colours they picked, 3 the first of them alone. speed: 0 normal, 1 slow,
|
||||
// 2 fast.
|
||||
struct IdentityStyle {
|
||||
std::string name;
|
||||
int mode{}, speed{};
|
||||
std::array<float, 3> from{}, to{};
|
||||
};
|
||||
std::vector<IdentityStyle> identity_styles;
|
||||
std::string identity_animation;
|
||||
float voice_range = default_voice_range; // how far the host (or server) forwards proximity voice
|
||||
// In a dedicated server's session: the server is the host but not a player.
|
||||
// Admins it lists may change its settings, and its map through Levels.
|
||||
|
||||
@@ -20,12 +20,12 @@ void report_board(std::uintptr_t entity, std::uint64_t steam_id, std::string_vie
|
||||
}
|
||||
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);
|
||||
logging::log(level, logging::Channel::customization, "Board items (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};
|
||||
const developer_board_detail::NativeColor value{color, 0, base + addr::native_cosmetics::color_rgb_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<std::uint64_t (*)(std::uintptr_t, const developer_board_detail::ParameterKey *,
|
||||
@@ -43,13 +43,26 @@ bool publish_materials(std::uintptr_t base, std::uintptr_t item) noexcept {
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
}
|
||||
// What each of the board's cosmetics does for this player: deck, grip tape, trucks, wheels.
|
||||
using Animations = std::array<developer_board_detail::ItemAnimation, developer_board_detail::part_count>;
|
||||
Animations board_animations(std::uint64_t steam_id, const multiplayer::MarkStyles &styles) noexcept {
|
||||
const auto mark = multiplayer::identity_mark(steam_id);
|
||||
Animations out;
|
||||
for (std::size_t i = 0; i < out.size(); ++i)
|
||||
out[i] = developer_board_detail::item_animation(mark, styles[multiplayer::skater_mark_items + i]);
|
||||
return out;
|
||||
}
|
||||
bool any_on(const Animations &animations) noexcept {
|
||||
return std::any_of(animations.begin(), animations.end(), [](const auto &animation) { return animation.on; });
|
||||
}
|
||||
} // 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 auto animation = developer_board_detail::animation_for(steam_id);
|
||||
const bool listed = animation != developer_board_detail::Animation::none;
|
||||
std::uint64_t generation, DeveloperBoardState &state,
|
||||
const multiplayer::MarkStyles &styles) noexcept {
|
||||
const auto animations = board_animations(steam_id, styles);
|
||||
const bool listed = any_on(animations);
|
||||
if (!board) { state = {}; return; }
|
||||
if (!listed && !state.count) return;
|
||||
if (!listed && state.colors.empty()) return;
|
||||
try {
|
||||
auto read = [](std::uintptr_t address, void *out, std::size_t size) { return memory::peek_bytes(address, out, size); };
|
||||
std::array<unsigned char, addr::native_cosmetics::publish_materials_prefix.size()> publisher{};
|
||||
@@ -65,14 +78,16 @@ void update_developer_board(std::uintptr_t base, std::uintptr_t board, std::uint
|
||||
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, animation, GetTickCount64(), write, publish);
|
||||
const auto wanted = static_cast<std::size_t>(std::count_if(live.bindings.begin(), live.bindings.begin() + live.count,
|
||||
[](const auto &binding) { return binding.eligible; }));
|
||||
developer_board_detail::animate(state, live, board, generation, animations, GetTickCount64(), write, publish);
|
||||
// The colours of the parts that are on: found, and taken.
|
||||
const auto wanted = std::count_if(live.bindings.begin(), live.bindings.end(),
|
||||
[&](const auto &binding) { return animations[binding.part].on; });
|
||||
const auto taken = std::count_if(state.colors.begin(), state.colors.end(),
|
||||
[&](const auto &saved) { return animations[saved.binding.part].on; });
|
||||
if (!published) report_board(board, steam_id, "native material publication failed", logging::Level::warning);
|
||||
else if (listed && !live.pending && state.count < wanted)
|
||||
else if (listed && !live.pending && taken < wanted)
|
||||
report_board(board, steam_id, "waiting for the remaining board color overrides", logging::Level::warning);
|
||||
else if (listed && state.count)
|
||||
report_board(board, steam_id, "animation active on the owned pink board cosmetics", logging::Level::info);
|
||||
else if (listed && taken) report_board(board, steam_id, "animation active on the board", logging::Level::info);
|
||||
} catch (const std::exception &error) {
|
||||
report_board(board, steam_id, error.what(), logging::Level::warning);
|
||||
} catch (...) {}
|
||||
@@ -82,15 +97,16 @@ void tick_local_developer_board(std::uintptr_t base, std::uintptr_t client, bool
|
||||
if (!ready) return;
|
||||
try {
|
||||
const auto social = multiplayer::steam_social_snapshot();
|
||||
const auto id = social && multiplayer::own_marks_shown() ? social->local.id : 0;
|
||||
if (developer_board_detail::animation_for(id) == developer_board_detail::Animation::none && !state.count) return;
|
||||
const auto id = social && multiplayer::own_items_shown() ? social->local.id : 0;
|
||||
const auto styles = developer_hoodie_detail::own_styles.load();
|
||||
if (!any_on(board_animations(id, styles)) && state.colors.empty()) 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);
|
||||
update_developer_board(base, board.entity, id, local.context, state, styles);
|
||||
} catch (...) {}
|
||||
}
|
||||
} // namespace dingosdk
|
||||
@@ -2,7 +2,10 @@
|
||||
#include "developer_board_material.h"
|
||||
|
||||
namespace dingosdk {
|
||||
// Colours the board a listed player rides: `styles` is how they have each marked cosmetic, the
|
||||
// board's deck, grip tape, trucks and wheels after the skater's.
|
||||
void update_developer_board(std::uintptr_t base, std::uintptr_t board, std::uint64_t steam_id,
|
||||
std::uint64_t generation, DeveloperBoardState &state) noexcept;
|
||||
std::uint64_t generation, DeveloperBoardState &state,
|
||||
const multiplayer::MarkStyles &styles) noexcept;
|
||||
void tick_local_developer_board(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept;
|
||||
} // namespace dingosdk
|
||||
@@ -4,11 +4,14 @@
|
||||
|
||||
namespace dingosdk {
|
||||
namespace developer_board_detail {
|
||||
using developer_hoodie_detail::Binding;
|
||||
using developer_hoodie_detail::Color;
|
||||
using developer_hoodie_detail::material_hash;
|
||||
using developer_hoodie_detail::Animation;
|
||||
using developer_hoodie_detail::animation_for;
|
||||
using developer_hoodie_detail::ItemAnimation;
|
||||
using developer_hoodie_detail::ParameterKey;
|
||||
using developer_hoodie_detail::animate;
|
||||
using developer_hoodie_detail::item_animation;
|
||||
using developer_hoodie_detail::item_color;
|
||||
using developer_hoodie_detail::material_hash;
|
||||
using developer_hoodie_detail::settle_publishes;
|
||||
inline constexpr std::uint64_t graphic_color = 0x873f48af36e50d59ULL;
|
||||
inline constexpr std::uint64_t base_color = 0x8c46cd354b18a9e2ULL;
|
||||
@@ -17,59 +20,40 @@ inline constexpr std::array<std::string_view, 3> slots{
|
||||
"Characters/Customization/Board/Slots/Skateboard_Deck_Slot",
|
||||
"Characters/Customization/Board/Slots/Skateboard_Trucks_Slot",
|
||||
"Characters/Customization/Board/Slots/Skateboard_Wheels_Slot"};
|
||||
// One colour of the board: a named material of the controller assigned one of `slots`, the
|
||||
// shader key, and which of the board's marked cosmetics it colours (deck, grip tape, trucks,
|
||||
// wheels: the order of their styles after the skater's, multiplayer::mark_item_names).
|
||||
struct Target {
|
||||
std::string_view asset, material;
|
||||
std::uint32_t recipe_slot;
|
||||
std::string_view material;
|
||||
std::size_t controller;
|
||||
std::uint64_t key;
|
||||
std::uint8_t part;
|
||||
};
|
||||
inline constexpr std::size_t part_count = multiplayer::mark_items - multiplayer::skater_mark_items;
|
||||
// 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.
|
||||
// Skateboard_base_mat / mat_skateboard_top, whatever deck is on it: use the live named map
|
||||
// identities. The deck's wood follows its graphic.
|
||||
inline constexpr std::array<Target, 5> 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},
|
||||
{"Deck_Mat", 0, graphic_color, 0},
|
||||
{"Deck_Mat", 0, base_color, 0},
|
||||
{"DeckTop_Mat", 0, graphic_color, 1},
|
||||
{"Truck_Mat", 1, base_color, 2},
|
||||
{"Wheel_mat", 2, base_color, 3},
|
||||
}};
|
||||
// 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{};
|
||||
};
|
||||
// The inline NativeValue layout used by addEsVector's setter. Its type is the live ColorRgb
|
||||
// descriptor, not a serialized Vec3 or boxed value.
|
||||
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<std::uint16_t>(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{};
|
||||
std::uint8_t settling{}; // publishes still owed after going back to `original`
|
||||
};
|
||||
inline constexpr std::size_t max_bindings = targets.size() * 4;
|
||||
struct Materials {
|
||||
std::uintptr_t component{}, appearance{};
|
||||
std::array<Binding, max_bindings> bindings{};
|
||||
std::size_t count{};
|
||||
bool pending{};
|
||||
};
|
||||
using developer_hoodie_detail::Materials;
|
||||
template<class Memory>
|
||||
std::uintptr_t parameter(Memory &m, std::uintptr_t material, std::uint64_t key) {
|
||||
const auto buckets = m.ptr(material, 0x268);
|
||||
@@ -89,21 +73,9 @@ template<class Read> Materials materials(Read &read, std::uintptr_t base, std::u
|
||||
using namespace multiplayer;
|
||||
CosmeticMemory<Read &> m{read, base};
|
||||
Materials out;
|
||||
m.check(m.ptr(entity) == base + addr::engine::board_entity_vtable, "RGB board actor type differs.");
|
||||
m.check(m.ptr(entity) == base + addr::engine::board_entity_vtable, "Board actor type differs.");
|
||||
out.component = m.component(entity);
|
||||
out.pending = m.template get<std::uint8_t>(out.component, 0x10a) || m.template get<std::uint8_t>(out.component, 0x10d);
|
||||
const auto recipe = m.ptr(out.component, 0x158);
|
||||
const auto count = m.count(recipe, sizeof(NativeCosmeticItem), max_cosmetic_slots);
|
||||
std::array<bool, targets.size()> eligible{};
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
const auto item = m.template get<NativeCosmeticItem>(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<std::uintptr_t>(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<std::uint32_t>(info) == 0x885eff59 &&
|
||||
@@ -130,8 +102,7 @@ template<class Read> Materials materials(Read &read, std::uintptr_t base, std::u
|
||||
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];
|
||||
for (const auto &target : targets) {
|
||||
if (target.controller != slot) continue;
|
||||
const auto hash = material_hash(target.material);
|
||||
auto node = m.ptr(buckets, (hash % size) * 8ULL);
|
||||
@@ -156,66 +127,17 @@ template<class Read> Materials materials(Read &read, std::uintptr_t base, std::u
|
||||
color = m.template get<Color>(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]};
|
||||
m.check(out.bindings.size() < max_bindings, "Board colors exceed bounds.");
|
||||
out.bindings.push_back({item, material, p, key, color, target.part});
|
||||
}
|
||||
}
|
||||
}
|
||||
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<developer_board_detail::SavedColor, developer_board_detail::max_bindings> 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.
|
||||
// What was saved of a board's parts while they are coloured. A key a part's material does not
|
||||
// have inherits .5 from the compiled board shaders: one is added at native preset priority, so
|
||||
// a later native preset replaces it, and goes back to that default when the colouring stops.
|
||||
// Never clear a NativeValue: the renderer still copies the key's full payload.
|
||||
template<class Write, class Publish>
|
||||
void animate(DeveloperBoardState &state, const Materials &live, std::uintptr_t entity, std::uint64_t generation,
|
||||
Animation animation, 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 = item_color(animation, milliseconds);
|
||||
std::array<std::uintptr_t, max_bindings> 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 (animation != Animation::none && 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)) {
|
||||
// Back to the part's own color, which takes a few publishes to show (settle_publishes):
|
||||
// `wrote` has the part published for each of them.
|
||||
wrote = binding.color == saved->original || write(binding, saved->original);
|
||||
const std::uint8_t left = saved->settling ? saved->settling - 1 : settle_publishes;
|
||||
if (!wrote) next.colors[next.count++] = *saved;
|
||||
else if (left) next.colors[next.count++] = {binding, saved->original, saved->original, left};
|
||||
}
|
||||
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
|
||||
struct DeveloperBoardState : developer_hoodie_detail::ItemState {};
|
||||
} // namespace dingosdk
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Extension/Multiplayer/Remote/native_skater.h"
|
||||
#include <Windows.h>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace dingosdk {
|
||||
namespace {
|
||||
@@ -23,7 +24,7 @@ void report_hoodie(std::uintptr_t entity, std::uint64_t steam_id, std::string_vi
|
||||
}
|
||||
if (entry->entity == entity && (entry->message == hash || now - entry->at < 5000)) return;
|
||||
*entry = {entity, now, hash};
|
||||
logging::log(level, logging::Channel::customization, "Developer hoodie (Steam {}, actor {:#x}): {}",
|
||||
logging::log(level, logging::Channel::customization, "Skater items (Steam {}, actor {:#x}): {}",
|
||||
steam_id, entity, message);
|
||||
}
|
||||
// NativeValue is owned by this material. Preserve its padding, type, priority,
|
||||
@@ -41,13 +42,34 @@ bool publish_materials(std::uintptr_t base, std::uintptr_t item) noexcept {
|
||||
return true;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
}
|
||||
// What each of the skater's marked cosmetics does for this player: top, bottoms, shoes, ...
|
||||
using Animations = std::array<developer_hoodie_detail::ItemAnimation, developer_hoodie_detail::slots.size()>;
|
||||
Animations skater_animations(std::uint64_t steam_id, const multiplayer::MarkStyles &styles) noexcept {
|
||||
const auto mark = multiplayer::identity_mark(steam_id);
|
||||
Animations out;
|
||||
for (std::size_t i = 0; i < out.size(); ++i) out[i] = developer_hoodie_detail::item_animation(mark, styles[i]);
|
||||
return out;
|
||||
}
|
||||
bool any_on(const Animations &animations) noexcept {
|
||||
return std::any_of(animations.begin(), animations.end(), [](const auto &animation) { return animation.on; });
|
||||
}
|
||||
// Which cosmetics are coloured, and how many colours of each: what is equipped decides.
|
||||
std::string coloured(const DeveloperHoodieState &state) {
|
||||
std::array<unsigned, developer_hoodie_detail::slots.size()> colors{};
|
||||
for (const auto &saved : state.colors) ++colors[saved.binding.part];
|
||||
std::string out;
|
||||
for (std::size_t i = 0; i < colors.size(); ++i)
|
||||
if (colors[i]) out += std::format("{}{} ({})", out.empty() ? "" : ", ", multiplayer::mark_item_names[i], colors[i]);
|
||||
return out.empty() ? "nothing equipped has a color to change" : "animation active on " + out;
|
||||
}
|
||||
} // namespace
|
||||
void update_developer_hoodie(std::uintptr_t base, std::uintptr_t entity, std::uint64_t steam_id,
|
||||
std::uint64_t generation, DeveloperHoodieState &state) noexcept {
|
||||
const auto animation = developer_hoodie_detail::animation_for(steam_id);
|
||||
const bool listed = animation != developer_hoodie_detail::Animation::none;
|
||||
std::uint64_t generation, DeveloperHoodieState &state,
|
||||
const multiplayer::MarkStyles &styles) noexcept {
|
||||
const auto animations = skater_animations(steam_id, styles);
|
||||
const bool listed = any_on(animations);
|
||||
if (!entity) { state = {}; return; }
|
||||
if (!listed && !state.count) return;
|
||||
if (!listed && state.colors.empty()) return;
|
||||
try {
|
||||
auto read = [](std::uintptr_t address, void *out, std::size_t size) { return memory::peek_bytes(address, out, size); };
|
||||
std::array<unsigned char, addr::native_cosmetics::publish_materials_prefix.size()> prefix{};
|
||||
@@ -58,13 +80,10 @@ void update_developer_hoodie(std::uintptr_t base, std::uintptr_t entity, std::ui
|
||||
}
|
||||
const auto live = developer_hoodie_detail::materials(read, base, entity);
|
||||
bool published = true;
|
||||
auto publish = [base, &published](std::uintptr_t item) { published = publish_materials(base, item); };
|
||||
developer_hoodie_detail::animate(state, live, entity, generation, animation, GetTickCount64(), write_color, publish);
|
||||
auto publish = [base, &published](std::uintptr_t item) { if (!publish_materials(base, item)) published = false; };
|
||||
developer_hoodie_detail::animate(state, live, entity, generation, animations, GetTickCount64(), write_color, publish);
|
||||
if (!published) report_hoodie(entity, steam_id, "native material publication failed", logging::Level::warning);
|
||||
else if (listed && state.count)
|
||||
report_hoodie(entity, steam_id, "animation active on the owned hoodie material", logging::Level::info);
|
||||
else if (listed && live.eligible)
|
||||
report_hoodie(entity, steam_id, "waiting for the assigned top slot's color parameters", logging::Level::warning);
|
||||
else if (listed && !live.pending) report_hoodie(entity, steam_id, coloured(state), logging::Level::info);
|
||||
} catch (const std::exception &error) {
|
||||
report_hoodie(entity, steam_id, error.what(), logging::Level::warning);
|
||||
} catch (...) {
|
||||
@@ -79,10 +98,11 @@ void tick_local_developer_hoodie(std::uintptr_t base, std::uintptr_t client, boo
|
||||
if (!ready) return;
|
||||
try {
|
||||
const auto social = multiplayer::steam_social_snapshot();
|
||||
const auto id = social && multiplayer::own_marks_shown() ? social->local.id : 0;
|
||||
if (developer_hoodie_detail::animation_for(id) == developer_hoodie_detail::Animation::none && !state.count) return;
|
||||
const auto id = social && multiplayer::own_items_shown() ? social->local.id : 0;
|
||||
const auto styles = developer_hoodie_detail::own_styles.load();
|
||||
if (!any_on(skater_animations(id, styles)) && state.colors.empty()) return;
|
||||
const auto local = multiplayer::capture_local(base, client, false);
|
||||
if (local.ready) update_developer_hoodie(base, local.entity, id, local.context, state);
|
||||
if (local.ready) update_developer_hoodie(base, local.entity, id, local.context, state, styles);
|
||||
else report_hoodie(client, id, local.detail, logging::Level::warning);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
namespace dingosdk {
|
||||
// Verified client update thread only. Remote identity comes from the Steam roster,
|
||||
// generation from the current native actor; neither is a client cosmetic parameter.
|
||||
// Colours what a listed player wears (developer_hoodie_material.h): `styles` is how they have
|
||||
// each marked cosmetic, the skater's first.
|
||||
void update_developer_hoodie(std::uintptr_t base, std::uintptr_t entity, std::uint64_t steam_id,
|
||||
std::uint64_t generation, DeveloperHoodieState &state) noexcept;
|
||||
std::uint64_t generation, DeveloperHoodieState &state,
|
||||
const multiplayer::MarkStyles &styles) noexcept;
|
||||
void tick_local_developer_hoodie(std::uintptr_t base, std::uintptr_t client, bool ready) noexcept;
|
||||
} // namespace dingosdk
|
||||
@@ -1,50 +1,93 @@
|
||||
#pragma once
|
||||
#include "Extension/Multiplayer/developer_identity.h"
|
||||
#include "Extension/Multiplayer/Remote/cosmetics.h"
|
||||
#include "Extension/Multiplayer/Remote/native_cosmetics_layout.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// The cosmetics of a player on one of the backend's lists (developer_identity.h) are coloured
|
||||
// for them: whatever they wear and ride, on their own material instances, as they choose on the
|
||||
// menu's Special page. It began as the developers' rainbow hoodie and pink board, which the
|
||||
// names here still say.
|
||||
namespace dingosdk {
|
||||
namespace developer_hoodie_detail {
|
||||
inline constexpr std::string_view asset = "Own_TopShirt_Gen_HoodieRelaxed_00001";
|
||||
// The compiled appearance preset names controllers by their assigned CAS slot.
|
||||
// "TopItem" is a Lua construction name, not the live controller's name.
|
||||
inline constexpr std::string_view top_slot = "Characters/MainCharacters/Generic/CAS/Common/Slots/CAS_Top_Slot";
|
||||
// Serialized ParamDbKey identities from this hoodie's AppearanceShaderExpressionPreset.
|
||||
// Only the six fabric color regions: textures, stitches and other clothing stay intact.
|
||||
// As seen in game: the first is the whole body, sleeves and hood; the fourth the cuffs and
|
||||
// hem; the fifth the drawstrings and hood lining. The other three do not show.
|
||||
inline constexpr std::array<std::uint64_t, 6> color_keys{
|
||||
0x8c46cd354b18a9e2ULL, 0x1567b5742f4ec0b2ULL, 0x1564a942cb33c53bULL,
|
||||
0xf526305270d94cb3ULL, 0x99879bf202d1960aULL, 0x998450425dbd9044ULL};
|
||||
// The compiled appearance preset names a skater's item controllers by their assigned CAS slot
|
||||
// ("TopItem" and the like are Lua construction names, not the live controllers' names).
|
||||
inline constexpr std::string_view slot_folder = "Characters/MainCharacters/Generic/CAS/Common/Slots/";
|
||||
// The skater's marked cosmetics, in the order of their styles (multiplayer::mark_item_names):
|
||||
// top, bottoms, shoes, socks, hat, glasses, outfit, costume.
|
||||
inline constexpr std::array<std::string_view, multiplayer::skater_mark_items> slots{
|
||||
"CAS_Top_Slot", "CAS_Bottom_Slot", "CAS_Footwear_Slot", "CAS_Sock_Slot",
|
||||
"CAS_Headgear_Slot", "CAS_Eyewear_Slot", "CAS_Outfit_Slot", "CAS_CostumeFullBody_Slot"};
|
||||
// Serialized ParamDbKey identities of the ColorRgb parameters the game's clothing presets set
|
||||
// (AppearanceShaderExpressionPreset.ParamInfo; read from 114 presets of every kind of item,
|
||||
// 2026-10-04): the colours of an item's regions. Every kind of clothing draws on the same
|
||||
// ones, the first thirteen by far the most, and each item colours some and leaves the rest at
|
||||
// the shaders' neutral grey (`neutral`), where its textures show as they are.
|
||||
// On the relaxed hoodie, as seen in game: the first is the whole body, sleeves and hood; the
|
||||
// fourth the cuffs and hem; the fifth the drawstrings and hood lining.
|
||||
inline constexpr std::array<std::uint64_t, 28> color_keys{
|
||||
0x8c46cd354b18a9e2ULL, 0x1567b5742f4ec0b2ULL, 0x1564a942cb33c53bULL, 0xf526305270d94cb3ULL,
|
||||
0x99879bf202d1960aULL, 0x998450425dbd9044ULL, 0x18984e85c679da8dULL, 0x2bf96fc9d8da57bbULL,
|
||||
0x13cfb51bd8474129ULL, 0x7c06880748ded394ULL, 0xfca496001ea60718ULL, 0xfca79ddc325249ceULL,
|
||||
0x5e1943a7c20ab77cULL, 0x0adafe3e13df4057ULL, 0xe3ab5f6a33dd1684ULL, 0xc31ad6ca613dcf43ULL,
|
||||
0xc1c70aa67f8bde7cULL, 0xc1c4554f6b58eee8ULL, 0x24e6723cc9f17eccULL, 0xd98b435969485311ULL,
|
||||
0x27e785e804edea5cULL, 0x27e45bf8d67d959fULL, 0xae823ac5acb2abbaULL, 0x89cbc6b088ae9b1bULL,
|
||||
0x593aacb9e12dcd6bULL, 0x4ec6aeb742a2c383ULL, 0xa19416e977675f88ULL, 0x0a94b0e28d102968ULL};
|
||||
using Color = std::array<float, 3>;
|
||||
inline constexpr Color neutral{.5f, .5f, .5f};
|
||||
// Native ParamDbKey, as a material's parameter node holds it. The first word is the final two
|
||||
// serialized GUID bytes; the type index is the live ColorRgb descriptor's.
|
||||
struct ParameterKey {
|
||||
std::uint16_t identity{}, size{}, type{}, count{};
|
||||
std::uint64_t hash{};
|
||||
};
|
||||
static_assert(sizeof(ParameterKey) == 16 && offsetof(ParameterKey, hash) == 8);
|
||||
// One colour of one material instance. `part` is which marked cosmetic it belongs to.
|
||||
struct Binding {
|
||||
std::uintptr_t material{}, node{}, type{};
|
||||
std::uint64_t key{};
|
||||
std::uintptr_t item{}, material{}, node{}; // node 0: no such parameter yet (the board adds one)
|
||||
ParameterKey key{};
|
||||
Color color{};
|
||||
bool operator==(const Binding &) const = default;
|
||||
std::uint8_t part{};
|
||||
};
|
||||
struct SavedColor {
|
||||
Binding binding;
|
||||
Color original{}, last{};
|
||||
std::uint8_t settling{}; // publishes still owed for `last` (settle_publishes)
|
||||
};
|
||||
inline constexpr std::size_t max_bindings = color_keys.size() * 4;
|
||||
// The colours found on an actor in one walk.
|
||||
struct Materials {
|
||||
std::uintptr_t component{}, appearance{}, item{};
|
||||
std::array<Binding, max_bindings> bindings{};
|
||||
std::size_t count{};
|
||||
bool eligible{};
|
||||
std::uintptr_t component{}, appearance{};
|
||||
std::vector<Binding> bindings;
|
||||
bool pending{};
|
||||
// Clothing: a colour at `neutral` is a region the item leaves to its textures, and is left
|
||||
// alone, unless the item colours none of its regions: then all of them are coloured.
|
||||
bool regions_only{};
|
||||
};
|
||||
// What was saved of an actor's colours while they are coloured here.
|
||||
struct ItemState {
|
||||
std::uintptr_t entity{}, component{}, appearance{};
|
||||
std::uint64_t generation{};
|
||||
std::vector<SavedColor> colors;
|
||||
};
|
||||
// What is equipped can be anything: this many colours of a skater, and no more.
|
||||
inline constexpr std::size_t max_bindings = 512;
|
||||
inline std::uint32_t material_hash(std::string_view name) noexcept {
|
||||
std::uint32_t hash = 5381;
|
||||
for (auto c : name) hash = hash * 33 ^ static_cast<unsigned char>(c >= 'A' && c <= 'Z' ? c + 32 : c);
|
||||
return hash;
|
||||
}
|
||||
inline Color rainbow(std::uint64_t milliseconds) noexcept {
|
||||
// One continuous hue cycle every eight seconds, in the material's existing gamut.
|
||||
const auto hue = static_cast<float>(milliseconds % 8000) * (6.f / 8000.f);
|
||||
inline Color rainbow(std::uint64_t milliseconds, std::uint64_t period = 8000) noexcept {
|
||||
// One continuous hue cycle every eight seconds (or `period`), in the material's existing gamut.
|
||||
const auto hue = static_cast<float>(milliseconds % period) * (6.f / static_cast<float>(period));
|
||||
const auto sector = static_cast<unsigned>(hue);
|
||||
constexpr float high = .8f, low = .025f;
|
||||
const auto rising = low + (high - low) * (hue - static_cast<float>(sector));
|
||||
@@ -58,164 +101,267 @@ inline Color rainbow(std::uint64_t milliseconds) noexcept {
|
||||
default: return {high, low, falling};
|
||||
}
|
||||
}
|
||||
// What a listed player's hoodie and board do: a developer's cycle the rainbow, a content
|
||||
// creator's go from deep red through bright red to a reddish pink and back, and a homie's
|
||||
// from deep gold through bright gold to yellow and back.
|
||||
enum class Animation : std::uint8_t { none, rainbow, red, gold };
|
||||
inline Animation animation_for(std::uint64_t steam_id) noexcept {
|
||||
using multiplayer::IdentityList;
|
||||
switch (multiplayer::identity_mark(steam_id).value_or(IdentityList::count)) {
|
||||
case IdentityList::developer: return Animation::rainbow;
|
||||
case IdentityList::content_creator: return Animation::red;
|
||||
case IdentityList::homie: return Animation::gold;
|
||||
default: return Animation::none;
|
||||
}
|
||||
// What one of a listed player's marked cosmetics does right now: nothing, the rainbow, or a
|
||||
// walk through three colours and back (one colour three times stands still).
|
||||
struct ItemAnimation {
|
||||
bool on{}, rainbow{};
|
||||
std::array<Color, 3> stops{};
|
||||
std::uint64_t period = 3000; // milliseconds for a round
|
||||
};
|
||||
// A colour as picked on screen, as the material takes it: linear, and inside the gamut the
|
||||
// rainbow keeps to.
|
||||
inline Color material_color(const std::array<std::uint8_t, 3> &picked) noexcept {
|
||||
Color out{};
|
||||
for (std::size_t i = 0; i < out.size(); ++i)
|
||||
out[i] = std::max(.004f, std::pow(static_cast<float>(picked[i]) / 255.f, 2.2f) * .8f);
|
||||
return out;
|
||||
}
|
||||
inline Color item_color(Animation animation, std::uint64_t milliseconds) noexcept {
|
||||
if (animation == Animation::rainbow) return rainbow(milliseconds);
|
||||
// One colour getting lighter and darker is hard to see, so each goes on into the colour
|
||||
// next to it: a deep shade, a bright one, then a reddish pink or a yellow, and back.
|
||||
constexpr std::array<Color, 3> red{{{.2f, .004f, .01f}, {.8f, .03f, .03f}, {.8f, .05f, .13f}}};
|
||||
constexpr std::array<Color, 3> gold{{{.34f, .12f, .006f}, {.8f, .5f, .06f}, {.8f, .64f, .11f}}};
|
||||
const auto &stops = animation == Animation::red ? red : gold;
|
||||
// 0 to 2 and back, every three seconds.
|
||||
const auto along = 1.f - std::cos(static_cast<float>(milliseconds % 3000) * (6.2831853f / 3000.f));
|
||||
const auto &from = stops[along < 1.f ? 0 : 1], &to = stops[along < 1.f ? 1 : 2];
|
||||
// The colours the pickers start from: about where each list's own animation begins and ends.
|
||||
inline std::pair<std::array<std::uint8_t, 3>, std::array<std::uint8_t, 3>> standard_picks(multiplayer::IdentityList mark) noexcept {
|
||||
using multiplayer::IdentityList;
|
||||
if (mark == IdentityList::content_creator) return {{0x88, 0x23, 0x23}, {0xff, 0x48, 0x70}};
|
||||
if (mark == IdentityList::homie) return {{0xad, 0x6c, 0x1c}, {0xff, 0xe6, 0x67}};
|
||||
return {{0x6e, 0x20, 0xff}, {0xb4, 0x78, 0xff}};
|
||||
}
|
||||
// What a cosmetic does for a player on `mark`'s list (nobody else's does anything) with the
|
||||
// style they chose on the Special page. Left standard, a developer's cycles the rainbow, a
|
||||
// content creator's goes from deep red through bright red to a reddish pink and back, and a
|
||||
// homie's from deep gold through bright gold to yellow and back. No style asks for the rainbow:
|
||||
// it is a developer's standard, and nobody else's.
|
||||
inline ItemAnimation item_animation(std::optional<multiplayer::IdentityList> mark, const multiplayer::MarkStyle &style) noexcept {
|
||||
using multiplayer::IdentityList;
|
||||
using multiplayer::MarkMode;
|
||||
ItemAnimation out;
|
||||
if (!mark || style.mode == MarkMode::off) return out;
|
||||
out.on = true;
|
||||
out.rainbow = style.mode == MarkMode::standard && *mark == IdentityList::developer;
|
||||
const std::uint64_t normal = out.rainbow ? 8000 : 3000;
|
||||
out.period = style.speed == 1 ? normal * 2 : style.speed == 2 ? normal / 2 : normal;
|
||||
if (style.mode == MarkMode::solid) {
|
||||
out.stops.fill(material_color(style.from));
|
||||
} else if (style.mode == MarkMode::gradient) {
|
||||
const auto from = material_color(style.from), to = material_color(style.to);
|
||||
out.stops = {from, Color{(from[0] + to[0]) * .5f, (from[1] + to[1]) * .5f, (from[2] + to[2]) * .5f}, to};
|
||||
} else if (!out.rainbow) {
|
||||
// One colour getting lighter and darker is hard to see, so each goes on into the colour
|
||||
// next to it: a deep shade, a bright one, then a reddish pink or a yellow.
|
||||
constexpr std::array<Color, 3> red{{{.2f, .004f, .01f}, {.8f, .03f, .03f}, {.8f, .05f, .13f}}};
|
||||
constexpr std::array<Color, 3> gold{{{.34f, .12f, .006f}, {.8f, .5f, .06f}, {.8f, .64f, .11f}}};
|
||||
out.stops = *mark == IdentityList::content_creator ? red : gold;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
inline Color item_color(const ItemAnimation &animation, std::uint64_t milliseconds) noexcept {
|
||||
if (animation.rainbow) return rainbow(milliseconds, animation.period);
|
||||
// 0 to 2 and back, once a round.
|
||||
const auto along = 1.f - std::cos(static_cast<float>(milliseconds % animation.period) *
|
||||
(6.2831853f / static_cast<float>(animation.period)));
|
||||
const auto &from = animation.stops[along < 1.f ? 0 : 1], &to = animation.stops[along < 1.f ? 1 : 2];
|
||||
const auto amount = along < 1.f ? along : along - 1.f;
|
||||
return {from[0] + (to[0] - from[0]) * amount, from[1] + (to[1] - from[1]) * amount,
|
||||
from[2] + (to[2] - from[2]) * amount};
|
||||
}
|
||||
// The local player's own styles (the Special page): sent with their appearance, and kept in
|
||||
// their profile as text, two hex digits a byte.
|
||||
inline std::atomic<multiplayer::MarkStyles> own_styles{};
|
||||
inline std::string mark_styles_text(const multiplayer::MarkStyles &styles) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
std::string text;
|
||||
const auto add = [&](std::uint8_t byte) { text += digits[byte >> 4], text += digits[byte & 15]; };
|
||||
for (const auto &style : styles) {
|
||||
add(static_cast<std::uint8_t>(style.mode));
|
||||
for (const auto part : style.from) add(part);
|
||||
for (const auto part : style.to) add(part);
|
||||
add(style.speed);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
inline std::optional<multiplayer::MarkStyles> parse_mark_styles(std::string_view text) noexcept {
|
||||
multiplayer::MarkStyles styles{};
|
||||
if (text.size() != styles.size() * 16) return std::nullopt;
|
||||
std::size_t at = 0;
|
||||
bool valid = true;
|
||||
const auto next = [&]() -> std::uint8_t {
|
||||
unsigned byte = 0;
|
||||
for (int digit = 0; digit < 2; ++digit) {
|
||||
const char c = text[at++];
|
||||
if (c >= '0' && c <= '9') byte = byte * 16 + static_cast<unsigned>(c - '0');
|
||||
else if (c >= 'a' && c <= 'f') byte = byte * 16 + static_cast<unsigned>(c - 'a' + 10);
|
||||
else valid = false;
|
||||
}
|
||||
return static_cast<std::uint8_t>(byte);
|
||||
};
|
||||
for (auto &style : styles) {
|
||||
style.mode = static_cast<multiplayer::MarkMode>(next());
|
||||
for (auto &part : style.from) part = next();
|
||||
for (auto &part : style.to) part = next();
|
||||
style.speed = next();
|
||||
valid = valid && multiplayer::valid_mark_style(style);
|
||||
}
|
||||
return valid ? std::optional(styles) : std::nullopt;
|
||||
}
|
||||
// A material instance's own parameters: a hash map of typed nodes.
|
||||
struct ParameterMap {
|
||||
std::uintptr_t buckets{}, stop{};
|
||||
std::uint32_t size{}, total{};
|
||||
};
|
||||
template<class Memory> ParameterMap parameter_map(Memory &m, std::uintptr_t material) {
|
||||
ParameterMap out{m.ptr(material, 0x268), 0, m.template get<std::uint32_t>(material, 0x270),
|
||||
m.template get<std::uint32_t>(material, 0x274)};
|
||||
if (!out.buckets || !out.size || out.size > 1024 || !out.total || out.total > 512) return {};
|
||||
out.stop = m.ptr(out.buckets, out.size * 8ULL);
|
||||
return out;
|
||||
}
|
||||
// The node of a material's ColorRgb parameter with this key, or 0. The EBX boxed ColorRgb has a
|
||||
// serialized gamut word; the loaded native ColorRgb is three floats (12 bytes): this goes by
|
||||
// the actual type descriptor, not the serialized asset size. A parameter of another shape is
|
||||
// passed over, not reported: what is equipped can be anything.
|
||||
template<class Memory>
|
||||
std::uintptr_t color_parameter(Memory &m, std::uintptr_t base, const ParameterMap &map, std::uint64_t key, ParameterKey &native,
|
||||
Color &color) {
|
||||
if (!map.size) return 0;
|
||||
auto node = m.ptr(map.buckets, (key % map.size) * 8ULL);
|
||||
for (std::uint32_t visited = 0; node && node != map.stop && visited < map.total; ++visited, node = m.ptr(node, 0x40)) {
|
||||
if (m.template get<std::uint64_t>(node, 8) != key) continue;
|
||||
native = m.template get<ParameterKey>(node);
|
||||
const auto type = m.ptr(node, 0x20);
|
||||
if (type < base || type >= base + 0x09144000 || native.size != sizeof(Color) || native.count != 1 ||
|
||||
!(m.template get<std::uint32_t>(node, 0x28) & 1) || m.template get<std::uint16_t>(type, 8) != native.type)
|
||||
return 0;
|
||||
const auto info = m.ptr(type);
|
||||
if (m.template get<std::uint32_t>(info) != 0x885eff59U || m.template get<std::uint16_t>(info, 6) != sizeof(Color)) return 0;
|
||||
color = m.template get<Color>(node, 0x10);
|
||||
return std::all_of(color.begin(), color.end(), [](float c) { return std::isfinite(c); }) ? node : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// All discovery is bounded and follows the actor's own component/controller/material
|
||||
// backlinks. Never edit the shared AppearanceShaderExpressionPreset or recipe.
|
||||
template<class Read> Materials materials(Read &read, std::uintptr_t base, std::uintptr_t entity) {
|
||||
using namespace multiplayer;
|
||||
CosmeticMemory<Read &> m{read, base};
|
||||
Materials out;
|
||||
m.check(m.ptr(entity) == base + addr::engine::skater_entity_vtable, "Hoodie actor type differs.");
|
||||
out.regions_only = true;
|
||||
m.check(m.ptr(entity) == base + addr::engine::skater_entity_vtable, "Skater actor type differs.");
|
||||
out.component = m.component(entity);
|
||||
const bool pending = m.template get<std::uint8_t>(out.component, 0x10a) || m.template get<std::uint8_t>(out.component, 0x10d);
|
||||
const auto recipe = m.ptr(out.component, 0x158);
|
||||
const auto items = m.count(recipe, sizeof(NativeCosmeticItem), max_cosmetic_slots);
|
||||
const auto wanted = cosmetic_asset_hash(asset);
|
||||
for (std::size_t i = 0; i < items; ++i) {
|
||||
const auto item = m.template get<NativeCosmeticItem>(recipe, i * sizeof(NativeCosmeticItem));
|
||||
if (item.hash == wanted) {
|
||||
// Native recipes may store only the hash (an empty CString).
|
||||
const auto name = m.text(reinterpret_cast<std::uintptr_t>(item.asset));
|
||||
out.eligible = !pending && (name.empty() || name == asset);
|
||||
}
|
||||
}
|
||||
out.pending = m.template get<std::uint8_t>(out.component, 0x10a) || m.template get<std::uint8_t>(out.component, 0x10d);
|
||||
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), "Hoodie controller array exceeds bounds.");
|
||||
(capacity - begin) / 8 <= 64 && (begin || !capacity), "Skater controller array exceeds bounds.");
|
||||
for (auto entry = begin; entry < end; entry += 8) {
|
||||
const auto item = m.ptr(entry);
|
||||
if (!item || m.ptr(item, 0x48) != out.appearance + 0x40 || m.text(m.ptr(item, 0x30)) != top_slot) continue;
|
||||
m.check(!out.item, "Hoodie has multiple top controllers.");
|
||||
out.item = item;
|
||||
if (!(m.template get<std::uint32_t>(item, 0xb0) & 2)) { out.eligible = false; continue; }
|
||||
if (!item || m.ptr(item, 0x48) != out.appearance + 0x40) continue;
|
||||
const auto name = m.text(m.ptr(item, 0x30));
|
||||
if (!name.starts_with(slot_folder)) continue;
|
||||
const auto slot = std::find(slots.begin(), slots.end(), std::string_view(name).substr(slot_folder.size()));
|
||||
if (slot == slots.end()) continue;
|
||||
const auto part = static_cast<std::size_t>(slot - slots.begin());
|
||||
// A slot with nothing in it, or with an item still on its way, has no materials.
|
||||
if (!(m.template get<std::uint32_t>(item, 0xb0) & 2)) continue;
|
||||
const auto buckets = m.ptr(item, 0x1f0);
|
||||
const auto size = m.template get<std::uint32_t>(item, 0x1f8), total = m.template get<std::uint32_t>(item, 0x1fc);
|
||||
if (!total) continue;
|
||||
m.check(size && size <= 256 && total <= 128, "Hoodie material map exceeds bounds.");
|
||||
const auto sentinel = m.ptr(buckets, size * 8ULL);
|
||||
const auto hash = material_hash("Top_mat");
|
||||
auto node = m.ptr(buckets, (hash % size) * 8ULL);
|
||||
for (std::uint32_t visited = 0; node && node != sentinel && visited < total; ++visited, node = m.ptr(node, 0x28)) {
|
||||
if (m.template get<std::uint32_t>(node) != hash) continue;
|
||||
std::array<std::uintptr_t, 4> seen{};
|
||||
for (std::size_t variant = 0; variant < seen.size(); ++variant) {
|
||||
const auto material = m.ptr(node, 8 + variant * 8);
|
||||
if (!material || std::find(seen.begin(), seen.begin() + variant, material) != seen.begin() + variant) continue;
|
||||
seen[variant] = material;
|
||||
const auto params = m.ptr(material, 0x268);
|
||||
const auto n = m.template get<std::uint32_t>(material, 0x270), count = m.template get<std::uint32_t>(material, 0x274);
|
||||
if (!count) continue;
|
||||
m.check(n && n <= 1024 && count <= 512, "Hoodie parameter map exceeds bounds.");
|
||||
const auto stop = m.ptr(params, n * 8ULL);
|
||||
for (const auto key : color_keys) {
|
||||
auto parameter = m.ptr(params, (key % n) * 8ULL);
|
||||
for (std::uint32_t v = 0; parameter && parameter != stop && v < count; ++v, parameter = m.ptr(parameter, 0x40)) {
|
||||
if (m.template get<std::uint64_t>(parameter, 8) != key) continue;
|
||||
// The EBX boxed ColorRgb has a serialized gamut word; the
|
||||
// loaded native ColorRgb is three floats (12 bytes). Validate
|
||||
// the actual type descriptor, not the serialized asset size.
|
||||
const auto type = m.ptr(parameter, 0x20);
|
||||
m.check(type >= base && type < base + 0x09144000 &&
|
||||
m.template get<std::uint16_t>(parameter, 2) == sizeof(Color) &&
|
||||
m.template get<std::uint32_t>(m.ptr(type)) == 0x885eff59U &&
|
||||
m.template get<std::uint16_t>(m.ptr(type), 6) == sizeof(Color) && m.template get<std::uint16_t>(parameter, 6) == 1 &&
|
||||
(m.template get<std::uint32_t>(parameter, 0x28) & 1) &&
|
||||
m.template get<std::uint16_t>(type, 8) == m.template get<std::uint16_t>(parameter, 4),
|
||||
"Hoodie color parameter type differs.");
|
||||
const auto color = m.template get<Color>(parameter, 0x10);
|
||||
m.check(std::all_of(color.begin(), color.end(), [](float c) { return std::isfinite(c); }) &&
|
||||
out.count < out.bindings.size(), "Hoodie color exceeds bounds.");
|
||||
out.bindings[out.count++] = {material, parameter, type, key, color};
|
||||
break;
|
||||
m.check(buckets && size && size <= 256 && total <= 128, "Skater material map exceeds bounds.");
|
||||
// Every named material of the item, whatever its mesh calls them ("Top_mat", "Bottom_mat", ...).
|
||||
std::array<std::uintptr_t, 257> heads{};
|
||||
m.check(read(buckets, heads.data(), (size + 1ULL) * 8), "Skater material map cannot be read.");
|
||||
std::uint32_t visited{};
|
||||
for (std::uint32_t bucket = 0; bucket < size; ++bucket) {
|
||||
for (auto node = heads[bucket]; node && node != heads[size]; node = m.ptr(node, 0x28)) {
|
||||
if (m.template get<std::uint32_t>(node) % size != bucket) break; // the next bucket's
|
||||
m.check(visited++ < total, "Skater material chain exceeds bounds.");
|
||||
std::array<std::uintptr_t, 4> seen{};
|
||||
for (std::size_t variant = 0; variant < seen.size(); ++variant) {
|
||||
const auto material = m.ptr(node, 8 + variant * 8);
|
||||
if (!material || std::find(seen.begin(), seen.begin() + variant, material) != seen.begin() + variant) continue;
|
||||
seen[variant] = material;
|
||||
const auto map = parameter_map(m, material);
|
||||
for (const auto key : color_keys) {
|
||||
if (out.bindings.size() == max_bindings) break;
|
||||
ParameterKey native;
|
||||
Color color{};
|
||||
if (const auto parameter = color_parameter(m, base, map, key, native, color))
|
||||
out.bindings.push_back({item, material, parameter, native, color, static_cast<std::uint8_t>(part)});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
inline bool same_binding(const Binding &a, const Binding &b) noexcept {
|
||||
return a.material == b.material && a.node == b.node && a.type == b.type && a.key == b.key;
|
||||
inline bool same_binding(const Binding &live, const Binding &saved) noexcept {
|
||||
// A parameter the board added has its node from the next fresh walk on.
|
||||
return live.item == saved.item && live.material == saved.material && live.key.hash == saved.key.hash &&
|
||||
(saved.node ? live.node == saved.node : live.node != 0);
|
||||
}
|
||||
} // namespace developer_hoodie_detail
|
||||
struct DeveloperHoodieState {
|
||||
std::uintptr_t entity{}, component{}, appearance{}, item{};
|
||||
std::uint64_t generation{};
|
||||
std::array<developer_hoodie_detail::SavedColor, developer_hoodie_detail::max_bindings> colors{};
|
||||
std::size_t count{};
|
||||
std::uint8_t settling{}; // publishes still owed after the animation went off (settle_publishes)
|
||||
};
|
||||
namespace developer_hoodie_detail {
|
||||
// The renderer shows a color one publish late. While the animation runs the next tick's
|
||||
// publish brings it; once it is off, the item's own colors need publishing again after the
|
||||
// write that brought them back, or the item keeps the last animated color on screen (seen
|
||||
// in game, 2026-10-04). The saved colors are kept for these extra publishes.
|
||||
// The renderer shows a color one publish late. While a color keeps changing the next tick's
|
||||
// publish brings it; one that stands still (a solid color, or the item's own once the
|
||||
// animation is off) needs publishing again after the write that set it, or the item keeps the
|
||||
// color before it on screen (seen in game, 2026-10-04).
|
||||
inline constexpr std::uint8_t settle_publishes = 3;
|
||||
// Writes are only to freshly rediscovered bindings. Restoration additionally
|
||||
// requires the current color to still be ours; a new native preset takes precedence.
|
||||
template<class Write, class Publish>
|
||||
void animate(DeveloperHoodieState &state, const Materials &live, std::uintptr_t entity, std::uint64_t generation,
|
||||
Animation animation, std::uint64_t milliseconds, Write &write, Publish &publish) {
|
||||
if (state.entity != entity || state.generation != generation || state.component != live.component ||
|
||||
state.appearance != live.appearance || state.item != live.item) state = {};
|
||||
const bool enabled = animation != Animation::none && live.eligible;
|
||||
const auto color = item_color(animation, milliseconds);
|
||||
const std::uint8_t settling = enabled ? 0 : state.settling ? state.settling - 1 : settle_publishes;
|
||||
DeveloperHoodieState next;
|
||||
next.settling = settling;
|
||||
if (enabled || settling) {
|
||||
next.entity = entity; next.generation = generation; next.component = live.component;
|
||||
next.appearance = live.appearance; next.item = live.item;
|
||||
}
|
||||
bool changed{};
|
||||
for (std::size_t i = 0; i < live.count; ++i) {
|
||||
// Ownership is discovered afresh before every write, and writes go only to what that walk
|
||||
// found. A color is restored only while it is still the one written here: a new native
|
||||
// preset takes precedence. `animations`: what each part does, or nothing.
|
||||
template<std::size_t Parts, class Write, class Publish>
|
||||
void animate(ItemState &state, const Materials &live, std::uintptr_t entity, std::uint64_t generation,
|
||||
const std::array<ItemAnimation, Parts> &animations, std::uint64_t milliseconds, Write &write, Publish &publish) {
|
||||
if (state.entity != entity || state.generation != generation || state.component != live.component || state.appearance != live.appearance)
|
||||
state = {};
|
||||
// Pausing, loading and a recipe on its way keep what was saved until a walk succeeds.
|
||||
if (live.pending) return;
|
||||
// What was saved of each color found: a walk finds them in the same order as the last.
|
||||
std::vector<const SavedColor *> saved(live.bindings.size());
|
||||
std::array<bool, Parts> regions{};
|
||||
for (std::size_t i = 0, from = 0; i < live.bindings.size(); ++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; }
|
||||
if (enabled) {
|
||||
const auto original = saved && binding.color == saved->last ? saved->original : binding.color;
|
||||
if (binding.color == color || write(binding, color)) {
|
||||
next.colors[next.count++] = {binding, original, color};
|
||||
changed = changed || binding.color != color;
|
||||
}
|
||||
} else if (saved && (binding.color == saved->last || binding.color == saved->original)) {
|
||||
if (binding.color != saved->original) write(binding, saved->original);
|
||||
changed = true;
|
||||
if (settling) next.colors[next.count++] = {binding, saved->original, saved->original};
|
||||
for (std::size_t n = 0; n < state.colors.size(); ++n) {
|
||||
const auto at = (from + n) % state.colors.size();
|
||||
if (same_binding(binding, state.colors[at].binding)) { saved[i] = &state.colors[at], from = at + 1; break; }
|
||||
}
|
||||
const auto &own = saved[i] && binding.color == saved[i]->last ? saved[i]->original : binding.color;
|
||||
regions[binding.part] = regions[binding.part] || own != neutral;
|
||||
}
|
||||
if (!next.count) next = {};
|
||||
state = next;
|
||||
if (changed) publish(live.item);
|
||||
ItemState next;
|
||||
next.entity = entity; next.generation = generation; next.component = live.component; next.appearance = live.appearance;
|
||||
next.colors.reserve(live.bindings.size());
|
||||
std::array<std::uintptr_t, 64> changed_items{};
|
||||
std::size_t changed{};
|
||||
for (std::size_t i = 0; i < live.bindings.size(); ++i) {
|
||||
const auto &binding = live.bindings[i];
|
||||
const auto *was = saved[i];
|
||||
const auto &animation = animations[binding.part];
|
||||
const bool ours = was && binding.color == was->last;
|
||||
const auto original = ours ? was->original : binding.color;
|
||||
bool show{};
|
||||
if (animation.on && !(live.regions_only && regions[binding.part] && original == neutral)) {
|
||||
const auto color = item_color(animation, milliseconds);
|
||||
if (binding.node && binding.color == color) {
|
||||
// Already this color: nothing to write, but it may still be owed to the renderer.
|
||||
const std::uint8_t left = ours ? was->settling : 0;
|
||||
next.colors.push_back({binding, original, color, static_cast<std::uint8_t>(left ? left - 1 : 0)});
|
||||
show = left != 0;
|
||||
} else if (write(binding, color)) {
|
||||
next.colors.push_back({binding, original, color, settle_publishes});
|
||||
show = true;
|
||||
} else if (was) next.colors.push_back(*was);
|
||||
} else if (was && (binding.color == was->last || binding.color == was->original)) {
|
||||
// Back to the item's own color, and published until it shows.
|
||||
const bool first = was->last != was->original;
|
||||
show = binding.color == was->original || write(binding, was->original);
|
||||
const std::uint8_t left = first ? settle_publishes : was->settling ? was->settling - 1 : 0;
|
||||
if (!show) next.colors.push_back(*was);
|
||||
else if (left) next.colors.push_back({binding, was->original, was->original, left});
|
||||
}
|
||||
if (show && changed < changed_items.size() &&
|
||||
std::find(changed_items.begin(), changed_items.begin() + changed, binding.item) == changed_items.begin() + changed)
|
||||
changed_items[changed++] = binding.item;
|
||||
}
|
||||
state.entity = next.entity, state.generation = next.generation, state.component = next.component, state.appearance = next.appearance;
|
||||
state.colors = std::move(next.colors);
|
||||
for (std::size_t i = 0; i < changed; ++i) publish(changed_items[i]);
|
||||
}
|
||||
} // namespace developer_hoodie_detail
|
||||
// What was saved of a skater's items while they are coloured.
|
||||
struct DeveloperHoodieState : developer_hoodie_detail::ItemState {};
|
||||
} // namespace dingosdk
|
||||
@@ -237,7 +237,7 @@ bool valid_appearance(const Appearance &a) noexcept {
|
||||
if (a.skater.key != skater_recipe_key || a.skater.version != 2 || a.board.key != board_recipe_key ||
|
||||
a.board.version != 1)
|
||||
return false;
|
||||
std::size_t size = header_size + 13;
|
||||
std::size_t size = header_size + 13 + mark_items * 8;
|
||||
for (const auto *r : {&a.skater, &a.board}) {
|
||||
if (r->scalars.size() > max_cosmetic_scalars || r->items.empty() ||
|
||||
r->items.size() > max_cosmetic_slots)
|
||||
@@ -445,7 +445,13 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
|
||||
w.integer(p.appearance.card.background, 4);
|
||||
w.integer(p.appearance.card.emblem, 4);
|
||||
w.integer(p.appearance.card.title, 4);
|
||||
w.integer(p.appearance.unmarked ? 1 : 0, 1);
|
||||
w.integer((p.appearance.hide_tag ? 1 : 0) | (p.appearance.hide_items ? 2 : 0), 1);
|
||||
for (const auto &style : p.appearance.marks) {
|
||||
w.integer(static_cast<std::uint8_t>(style.mode), 1);
|
||||
for (const auto part : style.from) w.integer(part, 1);
|
||||
for (const auto part : style.to) w.integer(part, 1);
|
||||
w.integer(style.speed, 1);
|
||||
}
|
||||
} else if (p.kind == PacketKind::audio) {
|
||||
w.integer(p.audio.size(), 2);
|
||||
AudioState previous;
|
||||
@@ -676,8 +682,17 @@ std::optional<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
|
||||
p.appearance.card.emblem = static_cast<std::uint32_t>(r.integer(4));
|
||||
p.appearance.card.title = static_cast<std::uint32_t>(r.integer(4));
|
||||
const auto flags = r.integer(1);
|
||||
p.appearance.unmarked = flags == 1;
|
||||
if (flags > 1 || !p.map || !valid_appearance(p.appearance))
|
||||
p.appearance.hide_tag = (flags & 1) != 0;
|
||||
p.appearance.hide_items = (flags & 2) != 0;
|
||||
bool styled = true;
|
||||
for (auto &style : p.appearance.marks) {
|
||||
style.mode = static_cast<MarkMode>(r.integer(1));
|
||||
for (auto &part : style.from) part = static_cast<std::uint8_t>(r.integer(1));
|
||||
for (auto &part : style.to) part = static_cast<std::uint8_t>(r.integer(1));
|
||||
style.speed = static_cast<std::uint8_t>(r.integer(1));
|
||||
styled = styled && valid_mark_style(style);
|
||||
}
|
||||
if (flags > 3 || !styled || !p.map || !valid_appearance(p.appearance))
|
||||
return {};
|
||||
} else if (p.kind == PacketKind::audio) {
|
||||
const auto count = r.integer(2);
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace dingosdk::multiplayer {
|
||||
constexpr std::size_t max_skater_bones = 512, max_board_bones = 64;
|
||||
constexpr std::size_t max_packet = 24576;
|
||||
constexpr std::size_t packet_header_size = 64;
|
||||
constexpr std::uint16_t protocol_version = 40;
|
||||
constexpr std::uint16_t protocol_version = 41;
|
||||
constexpr std::size_t max_throwdown_message = 4096;
|
||||
// Packet::tuning: the host's SkatePhysicsTuning differences (Extension/Skater/physics_tuning.h).
|
||||
constexpr std::size_t max_physics_tuning = 16384;
|
||||
|
||||
@@ -28,12 +28,31 @@ struct PlayerCard {
|
||||
std::uint32_t background{}, emblem{}, title{};
|
||||
bool operator==(const PlayerCard &) const = default;
|
||||
};
|
||||
// How one of a listed player's marked cosmetics is coloured (developer_identity.h), chosen on
|
||||
// the menu's Special page and carried in their Appearance, so everyone sees the same.
|
||||
enum class MarkMode : std::uint8_t { standard, off, gradient, solid };
|
||||
struct MarkStyle {
|
||||
MarkMode mode{}; // standard: what the player's list gives
|
||||
std::array<std::uint8_t, 3> from{}, to{}; // as picked on screen. gradient: between the two; solid: `from`
|
||||
std::uint8_t speed{}; // 0 normal, 1 slow, 2 fast
|
||||
bool operator==(const MarkStyle &) const = default;
|
||||
};
|
||||
// The marked cosmetics, in this order: whatever the skater has in each of these slots, then the
|
||||
// parts of whatever board they ride.
|
||||
inline constexpr std::array<std::string_view, 12> mark_item_names{
|
||||
"Top", "Bottoms", "Shoes", "Socks", "Hat", "Glasses", "Outfit", "Costume", "Deck", "Grip tape", "Trucks", "Wheels"};
|
||||
inline constexpr std::size_t mark_items = mark_item_names.size(), skater_mark_items = 8;
|
||||
using MarkStyles = std::array<MarkStyle, mark_items>;
|
||||
inline bool valid_mark_style(const MarkStyle &style) noexcept {
|
||||
return style.mode <= MarkMode::solid && style.speed <= 2;
|
||||
}
|
||||
struct Appearance {
|
||||
CosmeticRecipe skater, board;
|
||||
PlayerCard card;
|
||||
// The player has turned off the marks the ReSkate backend gives them (developer_identity.h):
|
||||
// nobody shows their tag or animates their hoodie and board.
|
||||
bool unmarked{};
|
||||
// The player has turned off what the ReSkate backend gives them (developer_identity.h):
|
||||
// nobody shows their tag, or nobody animates their hoodie and board.
|
||||
bool hide_tag{}, hide_items{};
|
||||
MarkStyles marks{};
|
||||
bool operator==(const Appearance &) const = default;
|
||||
};
|
||||
// Stand-ins for a peer's cosmetic that this PC does not have (a player's
|
||||
|
||||
@@ -257,7 +257,11 @@ void publish_party(Session &s) {
|
||||
s.custom_nametags = profile_runtime::local_preference("CustomNametags").value_or(true);
|
||||
s.chat_visible = profile_runtime::local_preference("ChatVisible").value_or(true);
|
||||
s.chat_filter = profile_runtime::local_preference("ChatFilter").value_or(true);
|
||||
show_own_marks(profile_runtime::local_preference("IdentityMarks").value_or(true));
|
||||
show_own_tag(profile_runtime::local_preference("IdentityTag").value_or(true));
|
||||
show_own_items(profile_runtime::local_preference("IdentityItems").value_or(true));
|
||||
if (const auto saved = profile_runtime::local_value("IdentityStyles"); saved && saved->is_string())
|
||||
if (const auto styles = developer_hoodie_detail::parse_mark_styles(saved->string()))
|
||||
developer_hoodie_detail::own_styles.store(*styles);
|
||||
apply_nametags(s);
|
||||
}
|
||||
// The party's limit: parties players form hold up to eight (the game's Party panel rows),
|
||||
@@ -427,7 +431,7 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
|
||||
};
|
||||
// The developer hoodie and board mark an identity: only one Steam vouches for (steam_vouched).
|
||||
const auto developer_id = [&](const Peer &p) {
|
||||
return steam_vouched(s, p) && shows_marks(p) ? p.member.id : std::uint64_t{};
|
||||
return steam_vouched(s, p) && shows_items(p) ? p.member.id : std::uint64_t{};
|
||||
};
|
||||
each_active_peer(s, [&](Peer &p) {
|
||||
// A dedicated server has no skater to show.
|
||||
@@ -437,8 +441,10 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
|
||||
// A far player between samples: their skater keeps its pose; sound, the label and
|
||||
// the spectate position are still refreshed (sound events are released one per frame).
|
||||
if (was_visible && local.ready && !p.render_failed && p.far_interval && now < p.next_far_sample) {
|
||||
update_developer_hoodie(s.base, remote_skater_entity(), developer_id(p), remote_skater_generation(), p.developer_hoodie);
|
||||
update_developer_board(s.base, remote_board_entity(), developer_id(p), remote_skater_generation(), p.developer_board);
|
||||
update_developer_hoodie(s.base, remote_skater_entity(), developer_id(p), remote_skater_generation(), p.developer_hoodie,
|
||||
mark_styles(p));
|
||||
update_developer_board(s.base, remote_board_entity(), developer_id(p), remote_skater_generation(), p.developer_board,
|
||||
mark_styles(p));
|
||||
present_audio(p);
|
||||
update_party_position(&p.render_pose);
|
||||
if (labels) label(p);
|
||||
@@ -515,9 +521,9 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui
|
||||
}
|
||||
}
|
||||
update_developer_hoodie(s.base, p.visible ? remote_skater_entity() : 0, developer_id(p),
|
||||
remote_skater_generation(), p.developer_hoodie);
|
||||
remote_skater_generation(), p.developer_hoodie, mark_styles(p));
|
||||
update_developer_board(s.base, p.visible ? remote_board_entity() : 0, developer_id(p),
|
||||
remote_skater_generation(), p.developer_board);
|
||||
remote_skater_generation(), p.developer_board, mark_styles(p));
|
||||
if (!p.visible) {
|
||||
stop_remote_audio();
|
||||
p.presented_audio.reset();
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <charconv>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <ctime>
|
||||
|
||||
@@ -324,14 +325,42 @@ std::string edit_nametag_style(Session &s, std::string_view argument) {
|
||||
profile_runtime::set_local_preference("CustomNametags", s.custom_nametags);
|
||||
return s.custom_nametags ? "ReSkate nametags: names, distances and dots." : "The game's own nametags.";
|
||||
}
|
||||
// The Special page: a player on one of the backend's lists going without their tag and items.
|
||||
// Their next appearance packet tells everyone (session_send.cpp).
|
||||
std::string edit_marks(Session &, std::string_view argument) {
|
||||
const auto shown = parse_switch(argument, own_marks_shown());
|
||||
if (!shown) return "Use on, off, or toggle for your tag and items.";
|
||||
show_own_marks(*shown);
|
||||
profile_runtime::set_local_preference("IdentityMarks", *shown);
|
||||
return *shown ? "Your tag and items show." : "Your tag and items are hidden, for you and everyone you skate with.";
|
||||
// The Special page: a player on one of the backend's lists going without their tag, or
|
||||
// without the animation on their items. Their next appearance packet tells everyone
|
||||
// (session_send.cpp).
|
||||
std::string edit_own_tag(Session &, std::string_view argument) {
|
||||
const auto shown = parse_switch(argument, own_tag_shown());
|
||||
if (!shown) return "Use on, off, or toggle for your tag.";
|
||||
show_own_tag(*shown);
|
||||
profile_runtime::set_local_preference("IdentityTag", *shown);
|
||||
return *shown ? "Your tag shows." : "Your tag is hidden, for you and everyone you skate with.";
|
||||
}
|
||||
std::string edit_own_items(Session &, std::string_view argument) {
|
||||
const auto shown = parse_switch(argument, own_items_shown());
|
||||
if (!shown) return "Use on, off, or toggle for your items.";
|
||||
show_own_items(*shown);
|
||||
profile_runtime::set_local_preference("IdentityItems", *shown);
|
||||
return *shown ? "Your items animate." : "Your items are plain, for you and everyone you skate with.";
|
||||
}
|
||||
// The Special page: how one of the player's marked cosmetics is coloured, as
|
||||
// "<cosmetic> <mode> <rrggbb> <rrggbb> <speed>" (MarkStyle). It goes out with their next
|
||||
// appearance packet and is kept in their profile.
|
||||
std::string edit_mark_style(Session &, std::string_view argument) {
|
||||
std::istringstream in{std::string(argument)};
|
||||
unsigned item{}, mode{}, from{}, to{}, speed{};
|
||||
if (!(in >> item >> mode >> std::hex >> from >> to >> std::dec >> speed) || item >= mark_items || mode > 3 ||
|
||||
from > 0xffffff || to > 0xffffff || speed > 2)
|
||||
return "That is not a cosmetic style.";
|
||||
const auto colour = [](unsigned value) {
|
||||
return std::array<std::uint8_t, 3>{static_cast<std::uint8_t>(value >> 16), static_cast<std::uint8_t>(value >> 8),
|
||||
static_cast<std::uint8_t>(value)};
|
||||
};
|
||||
auto styles = developer_hoodie_detail::own_styles.load();
|
||||
styles[item] = {static_cast<MarkMode>(mode), colour(from), colour(to), static_cast<std::uint8_t>(speed)};
|
||||
developer_hoodie_detail::own_styles.store(styles);
|
||||
profile_runtime::set_local_values({{"IdentityStyles", Json(developer_hoodie_detail::mark_styles_text(styles))}});
|
||||
return std::string(mark_item_names[item]) +
|
||||
(mode == 1 ? ": off." : mode == 2 ? ": your gradient." : mode == 3 ? ": your color." : ": back to the usual.");
|
||||
}
|
||||
// Hidden chat still receives lines, so showing it again brings back the conversation.
|
||||
std::string edit_chat_visible(Session &s, std::string_view argument) {
|
||||
@@ -445,14 +474,18 @@ std::string send_admin(Session &s, std::string text) {
|
||||
if (!send_packet(s, s.host_id, request, true, false)) return "Could not reach the server.";
|
||||
return "Sent to the server.";
|
||||
}
|
||||
// The Special page's commands. They are a player's own offline as well, where their hoodie and
|
||||
// board still animate.
|
||||
bool own_mark_command(std::string_view action) {
|
||||
return action == "mark-tag" || action == "mark-items" || action == "mark-style";
|
||||
}
|
||||
} // namespace
|
||||
bool queue_command(std::string_view action, std::string_view argument, std::string_view password) {
|
||||
// A player's own marks are theirs offline as well, where their hoodie and board still animate.
|
||||
if (launcher::offline_mode() && action != "marks") return false;
|
||||
if (launcher::offline_mode() && !own_mark_command(action)) return false;
|
||||
if ((action != "host" && action != "host-config" && action != "join" && action != "join-lobby" && action != "join-friend-lobby" && action != "stop" &&
|
||||
action != "distances" && action != "object-placement" && action != "kick" && action != "clear-objects" &&
|
||||
action != "nametags" && action != "nametag-style" && action != "chat-visible" && action != "chat-filter" &&
|
||||
action != "marks" &&
|
||||
!own_mark_command(action) &&
|
||||
action != "voice" && action != "voice-mute" &&
|
||||
action != "voice-volume" && action != "voice-allow" && action != "voice-range" && action != "chat" && action != "ban" && action != "unban" &&
|
||||
action != "world-layer-sync" && action != "noclip-allow" && action != "nobail-allow" && action != "boosts-allow" &&
|
||||
@@ -476,7 +509,7 @@ bool queue_command(std::string_view action, std::string_view argument, std::stri
|
||||
return true;
|
||||
}
|
||||
std::string command(std::string_view action, std::string_view argument, std::string_view password) {
|
||||
if (launcher::offline_mode() && action != "marks")
|
||||
if (launcher::offline_mode() && !own_mark_command(action))
|
||||
return "Multiplayer is unavailable in offline mode. Start Steam and relaunch ReSkate.";
|
||||
const bool configured_host = action == "host-config";
|
||||
if (configured_host) action = "host";
|
||||
@@ -632,7 +665,7 @@ std::string command(std::string_view action, std::string_view argument, std::str
|
||||
{"world-layer-sync", edit_world_layer_sync}, {"distances", edit_distances},
|
||||
{"nametags", edit_nametags},
|
||||
{"nametag-style", edit_nametag_style},
|
||||
{"marks", edit_marks},
|
||||
{"mark-tag", edit_own_tag}, {"mark-items", edit_own_items}, {"mark-style", edit_mark_style},
|
||||
{"chat-visible", edit_chat_visible}, {"chat-filter", edit_chat_filter}};
|
||||
for (const auto &[name, edit] : settings)
|
||||
if (action == name) {
|
||||
|
||||
@@ -310,11 +310,22 @@ inline bool dedicated_host(const Session &s) { return s.mode == Mode::join && ga
|
||||
inline bool steam_vouched(const Session &s, const Peer &peer) {
|
||||
return s.mode != Mode::join || peer.member.id == s.host_id || peer.direct_ready;
|
||||
}
|
||||
// Whether a player shows the marks the backend gives them: not until their appearance has
|
||||
// arrived, and not when it says they turned them off (Appearance::unmarked).
|
||||
inline bool shows_marks(const Peer &peer) {
|
||||
// Whether a player shows the tag the backend gives them, and whether the items that come with
|
||||
// it animate: not until their appearance has arrived, and not when it says they turned that
|
||||
// off (Appearance::hide_tag, Appearance::hide_items).
|
||||
inline bool shows_tag(const Peer &peer) {
|
||||
const auto &look = peer.appearance.value();
|
||||
return look && !look->unmarked;
|
||||
return look && !look->hide_tag;
|
||||
}
|
||||
inline bool shows_items(const Peer &peer) {
|
||||
const auto &look = peer.appearance.value();
|
||||
return look && !look->hide_items;
|
||||
}
|
||||
// How a player has each of their marked cosmetics animate: what their appearance says.
|
||||
inline const MarkStyles &mark_styles(const Peer &peer) {
|
||||
static const MarkStyles standard{};
|
||||
const auto &look = peer.appearance.value();
|
||||
return look ? look->marks : standard;
|
||||
}
|
||||
Peer *find_peer(Session &s, std::uint64_t id);
|
||||
unsigned player_count(const Session &s);
|
||||
|
||||
@@ -596,9 +596,13 @@ void send_local(Session &s, const NativeFrame &local, std::uint64_t now, std::ui
|
||||
if (now - s.last_cosmetic_capture >= 500000) {
|
||||
s.last_cosmetic_capture = now;
|
||||
auto appearance = capture_cosmetics(s.base, local, s.cosmetic_capture_status);
|
||||
// The player's choice to go without their marks travels with their outfit, so a change
|
||||
// is sent like one and reaches players who join later.
|
||||
if (appearance) appearance->unmarked = !own_marks_shown();
|
||||
// The player's choices to go without their tag or their animated items travel with their
|
||||
// outfit, so a change is sent like one and reaches players who join later.
|
||||
if (appearance) {
|
||||
appearance->hide_tag = !own_tag_shown();
|
||||
appearance->hide_items = !own_items_shown();
|
||||
appearance->marks = developer_hoodie_detail::own_styles.load();
|
||||
}
|
||||
if (appearance && (!s.sent_appearance || *appearance != *s.sent_appearance)) {
|
||||
auto p = packet(s, PacketKind::cosmetics, now);
|
||||
p.appearance = *appearance;
|
||||
|
||||
@@ -173,8 +173,28 @@ void publish(Session &s, const NativeFrame *local) {
|
||||
view.nametags = s.nametags;
|
||||
view.custom_nametags = s.custom_nametags;
|
||||
if (const auto social = steam_social_snapshot())
|
||||
if (const auto mark = identity_mark(social->local.id)) view.identity_tag = mark_role(*mark).second;
|
||||
view.identity_marks = own_marks_shown();
|
||||
if (const auto mark = identity_mark(social->local.id)) {
|
||||
std::tie(view.identity_tag_colour, view.identity_tag) = mark_role(*mark);
|
||||
view.identity_animation =
|
||||
*mark == IdentityList::developer ? "RAINBOW" : *mark == IdentityList::content_creator ? "RED" : "GOLD";
|
||||
const auto styles = developer_hoodie_detail::own_styles.load();
|
||||
const auto standard = developer_hoodie_detail::standard_picks(*mark);
|
||||
view.identity_styles.resize(styles.size());
|
||||
for (std::size_t i = 0; i < styles.size(); ++i) {
|
||||
// A cosmetic never given colours shows its list's own in the pickers.
|
||||
const bool picked = styles[i].mode == MarkMode::gradient || styles[i].mode == MarkMode::solid ||
|
||||
styles[i].from != styles[i].to || styles[i].from != std::array<std::uint8_t, 3>{};
|
||||
const auto &from = picked ? styles[i].from : standard.first, &to = picked ? styles[i].to : standard.second;
|
||||
auto &shown = view.identity_styles[i];
|
||||
shown.name = mark_item_names[i];
|
||||
shown.mode = static_cast<int>(styles[i].mode);
|
||||
shown.speed = styles[i].speed;
|
||||
for (std::size_t part = 0; part < 3; ++part)
|
||||
shown.from[part] = static_cast<float>(from[part]) / 255.f, shown.to[part] = static_cast<float>(to[part]) / 255.f;
|
||||
}
|
||||
}
|
||||
view.identity_tag_shown = own_tag_shown();
|
||||
view.identity_items_shown = own_items_shown();
|
||||
load_host_preferences(s);
|
||||
view.voice_range = s.mode == Mode::host ? s.voice_range
|
||||
: dedicated_host(s) ? s.roster_voice_range : s.host_preferences.voice_range;
|
||||
@@ -519,8 +539,8 @@ std::pair<std::uint32_t, std::string> player_role(Session &s, std::uint64_t send
|
||||
const auto *peer = local ? nullptr : find_peer(s, sender);
|
||||
const bool vouched = local || (peer && steam_vouched(s, *peer));
|
||||
// Who the backend says a player is comes before what they are in this lobby, unless they
|
||||
// have turned their marks off (the Special page), which their appearance tells everyone.
|
||||
if (vouched && (local ? own_marks_shown() : shows_marks(*peer)))
|
||||
// have turned their tag off (the Special page), which their appearance tells everyone.
|
||||
if (vouched && (local ? own_tag_shown() : shows_tag(*peer)))
|
||||
if (const auto mark = identity_mark(sender)) return mark_role(*mark);
|
||||
if (dedicated && (local ? s.server_admin : peer && peer->member.admin)) return {nametag_admin, "Admin"};
|
||||
if (!dedicated && (local ? s.mode == Mode::host : sender == s.host_id)) return {nametag_host, "Host"};
|
||||
|
||||
@@ -82,33 +82,41 @@ void lookup() {
|
||||
identity_mark(creator) == L::content_creator && identity_mark(homie) == L::homie && !identity_mark(stranger) &&
|
||||
!identity_mark(0),
|
||||
"A player's mark is not the first list they are on");
|
||||
check(own_marks_shown(), "A player's marks start hidden");
|
||||
show_own_marks(false);
|
||||
check(!own_marks_shown() && identity_mark(dev) == L::developer, "Hiding their own marks did not take, or changed the lists");
|
||||
show_own_marks(true);
|
||||
// Their tag and their items are each their own to hide.
|
||||
check(own_tag_shown() && own_items_shown(), "A player's tag or items start hidden");
|
||||
show_own_tag(false);
|
||||
check(!own_tag_shown() && own_items_shown() && identity_mark(dev) == L::developer,
|
||||
"Hiding their own tag did not take, hid their items, or changed the lists");
|
||||
show_own_tag(true);
|
||||
show_own_items(false);
|
||||
check(own_tag_shown() && !own_items_shown(), "Hiding their own items did not take, or hid their tag");
|
||||
show_own_items(true);
|
||||
check(publish_identity_lists({{{dev, other_dev}, {homie}, {creator}}}), "The lists did not go back");
|
||||
// Removing someone in the panel takes their mark away at the next refresh.
|
||||
check(publish_identity_lists({{{dev}, {}, {}}}) && reskate_developer(dev) && !reskate_developer(other_dev) &&
|
||||
!identity_listed(homie, L::homie),
|
||||
"Replaced lists kept a player who was removed");
|
||||
}
|
||||
// The hoodie and board: a developer's rainbow, a content creator's red, a homie's gold.
|
||||
// The hoodie and board: what a list gives a cosmetic, and what a player makes of it on the Special page.
|
||||
void items() {
|
||||
using namespace dingosdk::developer_hoodie_detail;
|
||||
publish_identity_lists({{{dev}, {dev, homie, creator}, {dev, creator}}});
|
||||
check(animation_for(dev) == Animation::rainbow && animation_for(creator) == Animation::red &&
|
||||
animation_for(homie) == Animation::gold,
|
||||
"A list did not give its animation, a developer's first and a content creator's before a homie's");
|
||||
check(animation_for(stranger) == Animation::none && animation_for(0) == Animation::none, "An unlisted player's items animate");
|
||||
check(item_color(Animation::rainbow, 0) == rainbow(0) && item_color(Animation::rainbow, 3000) == rainbow(3000),
|
||||
"The developer's rainbow changed");
|
||||
// Each goes from a deep shade to a bright one, on into the colour next to it, and back
|
||||
using dingosdk::multiplayer::MarkMode;
|
||||
using dingosdk::multiplayer::MarkStyle;
|
||||
using dingosdk::multiplayer::MarkStyles;
|
||||
const MarkStyle standard{};
|
||||
const auto spectrum = item_animation(L::developer, standard), red = item_animation(L::content_creator, standard),
|
||||
gold = item_animation(L::homie, standard);
|
||||
check(spectrum.on && spectrum.rainbow && red.on && !red.rainbow && gold.on && !gold.rainbow &&
|
||||
!item_animation(std::nullopt, standard).on,
|
||||
"A list did not give its animation, or a player on none got one");
|
||||
check(item_color(spectrum, 0) == rainbow(0) && item_color(spectrum, 3000) == rainbow(3000), "The developer's rainbow changed");
|
||||
// Red and gold go from a deep shade to a bright one, on into the colour next to it, and back
|
||||
// every three seconds.
|
||||
for (const auto animation : {Animation::red, Animation::gold}) {
|
||||
const auto deep = item_color(animation, 0), bright = item_color(animation, 750), far = item_color(animation, 1500);
|
||||
check(item_color(animation, 3000) == deep && item_color(animation, 4500) == far, "An animation does not repeat every three seconds");
|
||||
for (const auto *animation : {&red, &gold}) {
|
||||
const auto deep = item_color(*animation, 0), bright = item_color(*animation, 750), far = item_color(*animation, 1500);
|
||||
check(item_color(*animation, 3000) == deep && item_color(*animation, 4500) == far, "An animation does not repeat every three seconds");
|
||||
check(bright[0] > deep[0] * 2 && far[0] >= bright[0], "An animation does not go from deep to bright");
|
||||
if (animation == Animation::red) {
|
||||
if (animation == &red) {
|
||||
// A content creator's: red, then a reddish pink.
|
||||
check(bright[1] < bright[0] * .1f && bright[2] < bright[0] * .1f, "Red is not red");
|
||||
check(far[2] > bright[2] * 3 && far[2] < far[0] * .4f && far[1] < far[0] * .15f, "Red does not go on into a reddish pink");
|
||||
@@ -119,81 +127,177 @@ void items() {
|
||||
}
|
||||
for (std::uint64_t at = 0; at < 6000; at += 125) {
|
||||
// The material's gamut, and red always the strongest part of the colour.
|
||||
const auto color = item_color(animation, at);
|
||||
const auto color = item_color(*animation, at);
|
||||
check(color[0] <= .801f && color[1] > 0 && color[2] > 0 && color[0] > color[1] && color[0] > color[2],
|
||||
"An animation leaves its colours");
|
||||
}
|
||||
}
|
||||
|
||||
// A cosmetic turned off, and one given the player's own two colours.
|
||||
const MarkStyle off{MarkMode::off, {}, {}, 0}, own{MarkMode::gradient, {255, 0, 0}, {0, 0, 255}, 0};
|
||||
check(!item_animation(L::developer, off).on && !item_animation(L::homie, off).on, "A cosmetic turned off still animates");
|
||||
const auto custom = item_animation(L::homie, own);
|
||||
const auto first = item_color(custom, 0), last = item_color(custom, 1500), half = item_color(custom, 750);
|
||||
check(custom.on && !custom.rainbow && first[0] > .7f && first[2] < .01f && last[2] > .7f && last[0] < .01f && half[0] > .3f &&
|
||||
half[2] > .3f && item_color(custom, 3000) == first,
|
||||
"A player's own colours are not what their cosmetic moves between");
|
||||
// A developer can leave the rainbow for colours of their own; no style gives anyone else the rainbow.
|
||||
check(!item_animation(L::developer, own).rainbow && item_animation(L::developer, own).on, "A developer cannot have their own colours");
|
||||
// One colour of their own stands still, at any speed.
|
||||
const auto still = item_animation(L::homie, {MarkMode::solid, {0, 255, 0}, {9, 9, 9}, 2});
|
||||
check(still.on && !still.rainbow && item_color(still, 0) == material_color({0, 255, 0}) && item_color(still, 0)[1] > .7f &&
|
||||
item_color(still, 777) == item_color(still, 0) && item_color(still, 1500) == item_color(still, 0),
|
||||
"A solid colour is not the colour picked, or does not stand still");
|
||||
for (const auto mode : {MarkMode::standard, MarkMode::gradient, MarkMode::solid})
|
||||
check(!item_animation(L::content_creator, {mode, {}, {}, 0}).rainbow && !item_animation(L::homie, {mode, {}, {}, 0}).rainbow,
|
||||
"A style gave the rainbow to someone who is not a developer");
|
||||
// Slow takes twice as long and fast half, for the rainbow too.
|
||||
check(item_animation(L::homie, {MarkMode::gradient, {}, {}, 1}).period == 6000 &&
|
||||
item_animation(L::homie, {MarkMode::standard, {}, {}, 2}).period == 1500 &&
|
||||
item_animation(L::developer, {MarkMode::standard, {}, {}, 1}).period == 16000 &&
|
||||
item_animation(L::developer, {MarkMode::standard, {}, {}, 2}).period == 4000 &&
|
||||
item_color(item_animation(L::developer, {MarkMode::standard, {}, {}, 2}), 1000) == rainbow(2000),
|
||||
"The speed is not half or twice the usual");
|
||||
// The pickers start from colours close to the list's own.
|
||||
const auto picks = standard_picks(L::content_creator);
|
||||
const auto start = material_color(picks.first), finish = material_color(picks.second);
|
||||
check(std::abs(start[0] - .2f) < .02f && start[1] < .02f && std::abs(finish[0] - .8f) < .01f && std::abs(finish[2] - .13f) < .02f,
|
||||
"A content creator's pickers do not start from their red");
|
||||
|
||||
// The styles are kept in the profile as text.
|
||||
MarkStyles styles{};
|
||||
styles[0] = own, styles[2] = off, styles[4] = {MarkMode::gradient, {1, 2, 3}, {254, 253, 252}, 2};
|
||||
styles.back() = {MarkMode::solid, {7, 8, 9}, {}, 0};
|
||||
const auto text = mark_styles_text(styles);
|
||||
check(text.size() == styles.size() * 16 && parse_mark_styles(text) == styles && parse_mark_styles(mark_styles_text({})) == MarkStyles{},
|
||||
"The styles did not come back from their text");
|
||||
check(!parse_mark_styles("") && !parse_mark_styles("00") && !parse_mark_styles(std::string(text.size(), 'f')) &&
|
||||
!parse_mark_styles(std::string(text.size(), 'g')) && !parse_mark_styles(text + "00") &&
|
||||
!parse_mark_styles(std::string_view(text).substr(16)),
|
||||
"Text that is not styles was read as some");
|
||||
// Every marked cosmetic has a name, the skater's before the board's.
|
||||
check(dingosdk::multiplayer::mark_item_names.size() == slots.size() + dingosdk::developer_board_detail::part_count &&
|
||||
dingosdk::multiplayer::mark_item_names[slots.size()] == "Deck" && slots[2] == "CAS_Footwear_Slot",
|
||||
"The marked cosmetics and their names do not line up");
|
||||
}
|
||||
|
||||
// Turning the animation off (the Special page, or a list that no longer has the player) puts an
|
||||
// item's own colours back, and publishes them the extra times the renderer needs to show them.
|
||||
// Whatever a player wears is coloured where the item colours itself, each cosmetic on its own;
|
||||
// turning one off (the Special page, or a list that no longer has the player) puts its own
|
||||
// colours back, and publishes them the extra times the renderer needs to show them.
|
||||
void settling() {
|
||||
using namespace dingosdk::developer_hoodie_detail;
|
||||
using dingosdk::multiplayer::MarkMode;
|
||||
const auto red = item_animation(L::content_creator, {}), gold = item_animation(L::homie, {});
|
||||
const ItemAnimation none;
|
||||
using Skater = std::array<ItemAnimation, slots.size()>;
|
||||
const auto wearing = [&](const ItemAnimation &top, const ItemAnimation &shoes) {
|
||||
Skater out{};
|
||||
out[0] = top, out[2] = shoes;
|
||||
return out;
|
||||
};
|
||||
const Skater bare{};
|
||||
const Color black{.02f, .02f, .02f}, navy{.02f, .02f, .2f}, teal{.02f, .3f, .3f};
|
||||
// What the game holds for each colour region; the materials are read afresh each tick, as in game.
|
||||
std::array<Color, 2> held{black, navy};
|
||||
const auto bound = [](std::uintptr_t item, std::uintptr_t node, std::uint64_t key, const Color &color, std::uint8_t part) {
|
||||
return Binding{item, item + 40, node, {0, sizeof(Color), 1, 1, key}, color, part};
|
||||
};
|
||||
// What the game holds for each colour, read afresh each tick as in game: a top with two
|
||||
// regions it colours and one it leaves to its texture, and shoes that colour none of theirs.
|
||||
std::array<Color, 4> held{black, navy, neutral, neutral};
|
||||
dingosdk::DeveloperHoodieState state;
|
||||
Materials live;
|
||||
live.component = 1, live.appearance = 2, live.item = 3, live.eligible = true, live.count = 2;
|
||||
live.bindings[0] = {10, 100, 7, color_keys[0], black};
|
||||
live.bindings[1] = {10, 101, 7, color_keys[3], navy};
|
||||
int publishes{};
|
||||
live.component = 1, live.appearance = 2, live.regions_only = true;
|
||||
live.bindings = {bound(10, 100, color_keys[0], black, 0), bound(10, 101, color_keys[3], navy, 0),
|
||||
bound(10, 102, color_keys[7], neutral, 0), bound(11, 103, color_keys[0], neutral, 2)};
|
||||
std::vector<std::uintptr_t> published;
|
||||
int writes{};
|
||||
auto write = [&](const Binding &binding, const Color &color) {
|
||||
held[binding.node - 100] = color;
|
||||
++writes;
|
||||
return true;
|
||||
};
|
||||
auto publish = [&](std::uintptr_t) { ++publishes; };
|
||||
const auto tick = [&](Animation animation, std::uint64_t at) {
|
||||
auto publish = [&](std::uintptr_t item) { published.push_back(item); };
|
||||
const auto tick = [&](const Skater &animations, std::uint64_t at) {
|
||||
for (std::size_t i = 0; i < held.size(); ++i) live.bindings[i].color = held[i];
|
||||
animate(state, live, 5, 6, animation, at, write, publish);
|
||||
animate(state, live, 5, 6, animations, at, write, publish);
|
||||
};
|
||||
tick(Animation::red, 0);
|
||||
tick(Animation::red, 700);
|
||||
check(state.count == 2 && held[0] == item_color(Animation::red, 700) && held[1] == held[0] && publishes == 2,
|
||||
"The animation did not colour the hoodie");
|
||||
publishes = 0;
|
||||
tick(Animation::none, 800);
|
||||
check(held[0] == black && held[1] == navy && publishes == 1, "Turning it off did not bring the hoodie's own colours back");
|
||||
for (std::uint64_t at = 900; at < 910; ++at) tick(Animation::none, at);
|
||||
check(publishes == 1 + settle_publishes && !state.count && held[0] == black && held[1] == navy,
|
||||
"The own colours were not published the extra times, or the hoodie was not left alone after");
|
||||
using Items = std::vector<std::uintptr_t>;
|
||||
tick(wearing(red, none), 0);
|
||||
tick(wearing(red, none), 700);
|
||||
check(state.colors.size() == 2 && held[0] == item_color(red, 700) && held[1] == held[0] && held[2] == neutral &&
|
||||
held[3] == neutral && published == Items{10, 10},
|
||||
"The top's own regions were not coloured, or more than them was");
|
||||
// Shoes that colour none of their regions take the colour on all of them.
|
||||
tick(wearing(red, gold), 800);
|
||||
check(held[3] == item_color(gold, 800) && held[2] == neutral && held[0] == item_color(red, 800) && state.colors.size() == 3,
|
||||
"An item with no colours of its own was not coloured, or cosmetics did not each take their own");
|
||||
published.clear();
|
||||
tick(bare, 900);
|
||||
check(held[0] == black && held[1] == navy && held[3] == neutral && published == Items{10, 11},
|
||||
"Turning it off did not bring the items' own colours back");
|
||||
for (std::uint64_t at = 901; at < 911; ++at) tick(bare, at);
|
||||
check(published.size() == 2 * (1 + settle_publishes) && state.colors.empty() && held[0] == black && held[1] == navy,
|
||||
"The own colours were not published the extra times, or the items were not left alone after");
|
||||
// A colour the game itself sets meanwhile is the game's to keep.
|
||||
tick(Animation::gold, 1000);
|
||||
tick(wearing(gold, none), 1000);
|
||||
held[0] = teal;
|
||||
tick(Animation::none, 1100);
|
||||
tick(bare, 1100);
|
||||
check(held[0] == teal && held[1] == navy, "Turning it off overwrote a colour the game had set");
|
||||
// And it comes back on with the own colours still known.
|
||||
for (std::uint64_t at = 1200; at < 1210; ++at) tick(Animation::none, at);
|
||||
tick(Animation::red, 2000);
|
||||
tick(Animation::none, 2100);
|
||||
check(held[0] == teal && held[1] == navy, "A second round lost the hoodie's own colours");
|
||||
for (std::uint64_t at = 1200; at < 1210; ++at) tick(bare, at);
|
||||
tick(wearing(red, none), 2000);
|
||||
tick(bare, 2100);
|
||||
check(held[0] == teal && held[1] == navy, "A second round lost the top's own colours");
|
||||
for (std::uint64_t at = 2200; at < 2210; ++at) tick(bare, at);
|
||||
|
||||
// The board does the same, part by part.
|
||||
// A colour that stands still is written once, and published until the renderer shows it.
|
||||
const auto still = item_animation(L::homie, {MarkMode::solid, {0, 255, 0}, {}, 0});
|
||||
published.clear(), writes = 0;
|
||||
for (std::uint64_t at = 3000; at < 3010; ++at) tick(wearing(still, none), at);
|
||||
check(held[0] == item_color(still, 0) && held[1] == held[0] && writes == 2 && published == Items(1 + settle_publishes, 10),
|
||||
"A solid colour was written again and again, or not published until it showed");
|
||||
// While a recipe is on its way nothing is touched, and what was saved is kept.
|
||||
live.pending = true;
|
||||
tick(bare, 4000);
|
||||
check(held[0] == item_color(still, 0) && state.colors.size() == 2 && writes == 2, "A pending recipe was written to");
|
||||
live.pending = false;
|
||||
tick(bare, 4100);
|
||||
check(held[0] == teal && held[1] == navy, "The own colours were lost while a recipe was pending");
|
||||
|
||||
// The board does the same, each of its parts on its own: here the deck and the wheels, whose
|
||||
// material has no colour of its own until one is added.
|
||||
namespace board = dingosdk::developer_board_detail;
|
||||
std::array<Color, 2> painted{black, navy};
|
||||
std::array<Color, 2> painted{black, board::shader_default};
|
||||
dingosdk::DeveloperBoardState deck;
|
||||
board::Materials parts;
|
||||
parts.component = 1, parts.appearance = 2, parts.count = 2;
|
||||
parts.bindings[0] = {30, 10, 100, 7, board::parameter_key(board::base_color, 1), black, true};
|
||||
parts.bindings[1] = {31, 11, 101, 7, board::parameter_key(board::base_color, 1), navy, true};
|
||||
Materials parts;
|
||||
parts.component = 1, parts.appearance = 2;
|
||||
parts.bindings = {Binding{30, 10, 100, board::parameter_key(board::base_color, 1), black, 0},
|
||||
Binding{31, 11, 0, board::parameter_key(board::base_color, 1), board::shader_default, 3}};
|
||||
int shown{};
|
||||
auto paint = [&](const board::Binding &binding, const Color &color) {
|
||||
painted[binding.node - 100] = color;
|
||||
bool added{};
|
||||
auto paint = [&](const Binding &binding, const Color &color) {
|
||||
if (!binding.node) added = true; // the engine adds the missing parameter
|
||||
painted[binding.part ? 1 : 0] = color;
|
||||
return true;
|
||||
};
|
||||
auto show = [&](std::uintptr_t) { ++shown; };
|
||||
const auto roll = [&](Animation animation, std::uint64_t at) {
|
||||
for (std::size_t i = 0; i < painted.size(); ++i) parts.bindings[i].color = painted[i];
|
||||
board::animate(deck, parts, 5, 6, animation, at, paint, show);
|
||||
using Board = std::array<ItemAnimation, board::part_count>;
|
||||
const auto roll = [&](const ItemAnimation &on_deck, const ItemAnimation &on_wheels, std::uint64_t at) {
|
||||
parts.bindings[0].color = painted[0], parts.bindings[1].color = painted[1];
|
||||
parts.bindings[1].node = added ? 101 : 0;
|
||||
animate(deck, parts, 5, 6, Board{on_deck, none, none, on_wheels}, at, paint, show);
|
||||
};
|
||||
roll(Animation::gold, 0);
|
||||
roll(Animation::gold, 700);
|
||||
check(deck.count == 2 && painted[0] == item_color(Animation::gold, 700) && painted[1] == painted[0], "The animation did not colour the board");
|
||||
roll(gold, red, 0);
|
||||
roll(gold, red, 700);
|
||||
check(deck.colors.size() == 2 && added && painted[0] == item_color(gold, 700) && painted[1] == item_color(red, 700),
|
||||
"The board's parts did not each take their own animation, or a missing colour was not added");
|
||||
// The wheels alone turned off: back to the shaders' default, while the deck goes on.
|
||||
for (std::uint64_t at = 800; at < 810; ++at) roll(gold, none, at);
|
||||
check(painted[0] == item_color(gold, 809) && painted[1] == board::shader_default && deck.colors.size() == 1,
|
||||
"Turning one part off stopped another, or did not stop it");
|
||||
shown = 0;
|
||||
for (std::uint64_t at = 800; at < 810; ++at) roll(Animation::none, at);
|
||||
check(painted[0] == black && painted[1] == navy && shown == 2 * (1 + settle_publishes) && !deck.count,
|
||||
"Turning it off did not bring the board's own colours back and publish each part the extra times");
|
||||
for (std::uint64_t at = 900; at < 910; ++at) roll(none, none, at);
|
||||
check(painted[0] == black && painted[1] == board::shader_default && shown == 1 + settle_publishes && deck.colors.empty(),
|
||||
"Turning it off did not bring the board's own colours back and publish them the extra times");
|
||||
}
|
||||
|
||||
// --live <SteamID64>: the deployed backend, read the way the game reads it. Not
|
||||
|
||||
@@ -35,8 +35,10 @@ bool clear_lobby_guest_objects() { ++simulated_guest_wipes; return true; }
|
||||
namespace dingosdk {
|
||||
bool teleport_local_skater(const std::array<float, 3>&) { return true; }
|
||||
void update_board_lock(std::uintptr_t, std::uintptr_t, bool) noexcept {}
|
||||
void update_developer_hoodie(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperHoodieState &) noexcept {}
|
||||
void update_developer_board(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperBoardState &) noexcept {}
|
||||
void update_developer_hoodie(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperHoodieState &,
|
||||
const multiplayer::MarkStyles &) noexcept {}
|
||||
void update_developer_board(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperBoardState &,
|
||||
const multiplayer::MarkStyles &) noexcept {}
|
||||
}
|
||||
namespace dingosdk::multiplayer {
|
||||
// The backend's lists are not read here: a check lists the players it means.
|
||||
@@ -675,23 +677,43 @@ void role_checks() {
|
||||
"A developer on every list is not shown as a developer");
|
||||
check(player_role(host, id(third), false) == Role{nametag_creator, "Creator"},
|
||||
"A content creator who is also a homie is not shown as a creator");
|
||||
// A player who has turned their marks off (the Special page) is whatever they are in the lobby,
|
||||
// to everyone: their appearance carries the choice through the host.
|
||||
// A player who has turned their tag off (the Special page) is whatever they are in the lobby,
|
||||
// to everyone: their appearance carries the choice through the host. Their items are a
|
||||
// separate choice, which leaves the tag alone.
|
||||
auto plain = packet(host, PacketKind::cosmetics, sim.now);
|
||||
plain.appearance = {{skater_recipe_key, 2, {}, {{1, "Outfit0", {}}}}, {board_recipe_key, 1, {}, {{2, "Deck", {}}}}};
|
||||
plain.appearance.unmarked = true;
|
||||
plain.appearance.hide_items = true;
|
||||
host.cosmetic_packet = encode_wire(plain);
|
||||
broadcast(host, plain, true, false, sim.now);
|
||||
sim.run(20);
|
||||
const auto told = [&](const Session &s) {
|
||||
const auto *peer = find_peer(const_cast<Session &>(s), id(host));
|
||||
return peer && !shows_items(*peer) && shows_tag(*peer);
|
||||
};
|
||||
check(told(first) && told(second) && player_role(first, id(host), false) == Role{nametag_homie, "Homie"},
|
||||
"A homie who turned their items off was not seen to, or lost their tag");
|
||||
// A later packet, as the host's game would send after the change.
|
||||
const auto look = plain.appearance;
|
||||
plain = packet(host, PacketKind::cosmetics, sim.now);
|
||||
plain.appearance = look;
|
||||
plain.appearance.hide_items = false, plain.appearance.hide_tag = true;
|
||||
host.cosmetic_packet = encode_wire(plain);
|
||||
broadcast(host, plain, true, false, sim.now);
|
||||
sim.run(20);
|
||||
check(player_role(first, id(host), false) == Role{nametag_host, "Host"} &&
|
||||
player_role(second, id(host), false) == Role{nametag_host, "Host"},
|
||||
"A homie who turned their marks off still shows as a homie");
|
||||
check(player_role(host, id(first), false) == Role{nametag_creator, "Creator"}, "One player's choice hid another's marks");
|
||||
show_own_marks(false);
|
||||
"A homie who turned their tag off still shows as a homie");
|
||||
const auto *shown = find_peer(first, id(host));
|
||||
check(shown && shows_items(*shown), "Turning their tag off turned a player's items off");
|
||||
check(player_role(host, id(first), false) == Role{nametag_creator, "Creator"}, "One player's choice hid another's tag");
|
||||
show_own_items(false);
|
||||
check(player_role(first, id(first), true) == Role{nametag_creator, "Creator"}, "Turning their items off hid a player's own tag");
|
||||
show_own_items(true);
|
||||
show_own_tag(false);
|
||||
check(player_role(first, id(first), true) == Role{nametag_white, {}} &&
|
||||
player_role(host, id(first), false) == Role{nametag_creator, "Creator"},
|
||||
"A player's own choice did not hide their marks from themselves, or hid them from others before they were told");
|
||||
show_own_marks(true);
|
||||
"A player's own choice did not hide their tag from themselves, or hid it from others before they were told");
|
||||
show_own_tag(true);
|
||||
// A chat line carries its sender's role.
|
||||
check(send_chat(first, "new video is up").empty(), "A guest's chat was refused");
|
||||
sim.run(20);
|
||||
|
||||
@@ -331,14 +331,30 @@ void cosmetics_codec() {
|
||||
{board_recipe_key, 1, {0x3f800000}, {{13, "Own_Deck", {7}}}}};
|
||||
const auto bytes = encode(p);
|
||||
const auto decoded = decode(bytes);
|
||||
check(decoded && decoded->appearance == p.appearance && !decoded->appearance.unmarked,
|
||||
check(decoded && decoded->appearance == p.appearance && !decoded->appearance.hide_tag && !decoded->appearance.hide_items,
|
||||
"Cosmetic fields or opaque parameter bits were lost");
|
||||
// A player's choice to go without their backend marks travels with their outfit.
|
||||
auto unmarked = p;
|
||||
unmarked.appearance.unmarked = true;
|
||||
const auto told = decode(encode(unmarked));
|
||||
check(told && told->appearance.unmarked && told->appearance == unmarked.appearance && !(told->appearance == p.appearance),
|
||||
"A player's choice to hide their marks was lost");
|
||||
// A player's choices to go without their backend tag, or its animated items, travel with
|
||||
// their outfit, each on its own.
|
||||
for (const auto &[tag, items] : {std::pair{true, false}, std::pair{false, true}, std::pair{true, true}}) {
|
||||
auto hidden = p;
|
||||
hidden.appearance.hide_tag = tag, hidden.appearance.hide_items = items;
|
||||
const auto told = decode(encode(hidden));
|
||||
check(told && told->appearance.hide_tag == tag && told->appearance.hide_items == items &&
|
||||
told->appearance == hidden.appearance && !(told->appearance == p.appearance),
|
||||
"A player's choice to hide their tag or their items was lost");
|
||||
}
|
||||
// So does how they have each marked cosmetic animate.
|
||||
auto styled = p;
|
||||
styled.appearance.marks[0] = {MarkMode::gradient, {1, 2, 3}, {250, 251, 252}, 2};
|
||||
styled.appearance.marks[4] = {MarkMode::off, {}, {}, 1};
|
||||
styled.appearance.marks.back() = {MarkMode::solid, {9, 8, 7}, {}, 0};
|
||||
const auto kept = decode(encode(styled));
|
||||
check(kept && kept->appearance == styled.appearance && kept->appearance.marks[0].to[2] == 252 &&
|
||||
kept->appearance.marks.back().mode == MarkMode::solid && kept->appearance.marks.back().from[0] == 9 &&
|
||||
kept->appearance.marks[1] == MarkStyle{} && !(kept->appearance == p.appearance),
|
||||
"A player's cosmetic styles were lost");
|
||||
check(!valid_mark_style({static_cast<MarkMode>(4), {}, {}, 0}) && !valid_mark_style({MarkMode::standard, {}, {}, 3}),
|
||||
"A cosmetic style no menu can make was accepted");
|
||||
for (std::size_t n = 0; n < bytes.size(); ++n)
|
||||
check(!decode(std::span(bytes).first(n)), "Truncated cosmetics accepted");
|
||||
auto corrupt = bytes;
|
||||
|
||||
@@ -33,11 +33,14 @@ inline std::optional<IdentityList> identity_mark(std::uint64_t id) noexcept {
|
||||
if (identity_listed(id, list)) return list;
|
||||
return std::nullopt;
|
||||
}
|
||||
// The local player's own choice to go without their marks (the menu's Special page, which
|
||||
// only a listed player gets). Their appearance tells everyone else (Appearance::unmarked).
|
||||
inline std::atomic<bool> own_marks{true};
|
||||
inline bool own_marks_shown() noexcept { return own_marks.load(std::memory_order_relaxed); }
|
||||
inline void show_own_marks(bool shown) noexcept { own_marks.store(shown, std::memory_order_relaxed); }
|
||||
// The local player's own choices to go without their tag, and without their animated items
|
||||
// (the menu's Special page, which only a listed player gets). Their appearance tells everyone
|
||||
// else (Appearance::hide_tag, Appearance::hide_items).
|
||||
inline std::atomic<bool> own_tag{true}, own_items{true};
|
||||
inline bool own_tag_shown() noexcept { return own_tag.load(std::memory_order_relaxed); }
|
||||
inline void show_own_tag(bool shown) noexcept { own_tag.store(shown, std::memory_order_relaxed); }
|
||||
inline bool own_items_shown() noexcept { return own_items.load(std::memory_order_relaxed); }
|
||||
inline void show_own_items(bool shown) noexcept { own_items.store(shown, std::memory_order_relaxed); }
|
||||
// A global ban is enforced by whoever a player would play with: a host and a dedicated server
|
||||
// turn them away, and a guest leaves a lobby they host, which their own game does not start
|
||||
// either (session_receive.cpp, Server/server_host.cpp). A server can opt out ("global_bans"),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "multiplayer_menu_internal.h"
|
||||
#include "role_badge.h"
|
||||
#include "Engine/Game/World/world_names.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
#include <Windows.h>
|
||||
|
||||
namespace dingosdk::overlay {
|
||||
@@ -267,21 +271,129 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
|
||||
void multiplayer_network_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
|
||||
debug_page(menu, model.multiplayer, callbacks);
|
||||
}
|
||||
// SPECIAL: the page only a player on one of the backend's lists gets. Their tag, and the hoodie
|
||||
// and board that come with it, on or off for everyone at once.
|
||||
// SPECIAL: the page only a player on one of the backend's lists gets. Their tag as everyone
|
||||
// sees it, and whether it shows; then the items that come with it, on or off together and
|
||||
// each on its own: what their list gives it, two colours of their own, or nothing.
|
||||
void special_page(SkateMenu &menu, const Model &model, const CallbacksV3 &) {
|
||||
const auto &mp = model.multiplayer;
|
||||
std::array<char, 65> unused{};
|
||||
ImGui::BeginChild("special", ImVec2(0, page_body_height(menu)));
|
||||
begin_card(menu, "special-marks", "YOUR TAG", mp.identity_tag.c_str());
|
||||
bool shown = mp.identity_marks;
|
||||
if (toggle_row(menu, "Show my tag and items",
|
||||
"Your tag in chat and on your nametag, and the animated hoodie and board that come with it. "
|
||||
"Off: you look like any other player, to yourself and to everyone you skate with.",
|
||||
shown)) {
|
||||
std::array<char, 65> unused{};
|
||||
send_private(menu, "marks", shown ? "on" : "off", unused, false);
|
||||
begin_card(menu, "special-tag", "YOUR TAG");
|
||||
{
|
||||
// Drawn the way chat and nametags draw it (role_badge.h), and faint while it is off.
|
||||
field(menu, "Looks like", "Your tag and name, as chat and your nametag show them.");
|
||||
const std::string name = mp.local_name.empty() ? std::string("You") : mp.local_name;
|
||||
const float size = px(18), height = ImGui::GetFrameHeight(), alpha = mp.identity_tag_shown ? 1.0f : .3f;
|
||||
auto *draw = ImGui::GetWindowDrawList();
|
||||
const auto at = ImGui::GetCursorScreenPos();
|
||||
const float badge = overlay::detail::role_badge_width(menu.bold, size, mp.identity_tag) + px(6);
|
||||
overlay::detail::draw_role_badge(draw, menu.bold, size, at, height, mp.identity_tag, mp.identity_tag_colour, alpha);
|
||||
const auto extent = menu.bold->CalcTextSizeA(size, FLT_MAX, 0, name.c_str());
|
||||
const ImVec2 name_at(at.x + badge, at.y + (height - extent.y) * .5f);
|
||||
const int name_vertices = draw->VtxBuffer.Size;
|
||||
draw->PushClipRect(at, ImVec2(at.x + ImGui::GetContentRegionAvail().x, at.y + height), true);
|
||||
draw->AddText(menu.bold, size, name_at,
|
||||
(mp.identity_tag_colour & ~IM_COL32_A_MASK) | (static_cast<ImU32>(255.0f * alpha) << IM_COL32_A_SHIFT), name.c_str());
|
||||
draw->PopClipRect();
|
||||
overlay::detail::shade_nametag_gradient(draw, name_vertices, name_at.x, extent.x, mp.identity_tag_colour, ImGui::GetTime());
|
||||
ImGui::Dummy(ImVec2(std::min(badge + extent.x, ImGui::GetContentRegionAvail().x), height));
|
||||
}
|
||||
bool tag = mp.identity_tag_shown;
|
||||
if (toggle_row(menu, "Show my tag",
|
||||
"Your tag and the color of your name, in chat and on your nametag. "
|
||||
"Off: your name looks like any other player's, to yourself and to everyone you skate with.",
|
||||
tag))
|
||||
send_private(menu, "mark-tag", tag ? "on" : "off", unused, false);
|
||||
end_card();
|
||||
|
||||
begin_card(menu, "special-items", "YOUR ITEMS");
|
||||
bool shown = mp.identity_items_shown;
|
||||
if (toggle_row(menu, "Show my items",
|
||||
"The colors that come with your tag, on whatever you wear and ride. "
|
||||
"Off: it all keeps its own colors, for you and for everyone you skate with.",
|
||||
shown))
|
||||
send_private(menu, "mark-items", shown ? "on" : "off", unused, false);
|
||||
note("Below, each one on its own: it colors whatever you have on in that slot. Everyone you skate with sees what you pick.");
|
||||
end_card();
|
||||
|
||||
// The colours being picked: held here while the mouse has them, and after it lets go until
|
||||
// the saved ones catch up, so a swatch never jumps back to the colour it had.
|
||||
struct Picking {
|
||||
std::array<float, 3> from{}, to{};
|
||||
bool changed{};
|
||||
int waiting{}; // frames
|
||||
};
|
||||
static std::vector<Picking> picking;
|
||||
picking.resize(mp.identity_styles.size());
|
||||
for (int item = 0; item < static_cast<int>(mp.identity_styles.size()); ++item) {
|
||||
const auto &style = mp.identity_styles[static_cast<std::size_t>(item)];
|
||||
auto &picked = picking[static_cast<std::size_t>(item)];
|
||||
const auto same = [](const std::array<float, 3> &a, const std::array<float, 3> &b) {
|
||||
return std::abs(a[0] - b[0]) < .003f && std::abs(a[1] - b[1]) < .003f && std::abs(a[2] - b[2]) < .003f;
|
||||
};
|
||||
if (picked.waiting && same(picked.from, style.from) && same(picked.to, style.to)) picked.waiting = 0;
|
||||
else if (picked.waiting) --picked.waiting;
|
||||
if (!picked.changed && !picked.waiting) picked.from = style.from, picked.to = style.to;
|
||||
const auto send = [&](int mode, int speed) {
|
||||
const auto hex = [](const std::array<float, 3> &colour) {
|
||||
unsigned value{};
|
||||
for (const float part : colour) value = value << 8 | static_cast<unsigned>(std::clamp(part, 0.0f, 1.0f) * 255.0f + 0.5f);
|
||||
return value;
|
||||
};
|
||||
send_private(menu, "mark-style", std::format("{} {} {:06x} {:06x} {}", item, mode, hex(picked.from), hex(picked.to), speed),
|
||||
unused, false);
|
||||
};
|
||||
// The card is headed by the cosmetic: whatever is in that slot is what gets coloured.
|
||||
std::string title = style.name;
|
||||
for (auto &letter : title) letter = static_cast<char>(std::toupper(static_cast<unsigned char>(letter)));
|
||||
ImGui::PushID(item);
|
||||
begin_card(menu, title.c_str(), title.c_str());
|
||||
// The options in the order shown; a style's mode is 0 what the list gives, 1 off, 2 a
|
||||
// gradient between the player's two colours, 3 their one colour.
|
||||
static constexpr std::array<int, 4> modes{0, 2, 3, 1};
|
||||
int option = static_cast<int>(std::find(modes.begin(), modes.end(), style.mode) - modes.begin()) % 4;
|
||||
field(menu, "Color");
|
||||
if (choice(menu, "mode", option, {mp.identity_animation.c_str(), "GRADIENT", "SOLID", "OFF"}, shown))
|
||||
send(modes[static_cast<std::size_t>(option)], style.speed);
|
||||
if (style.mode == 2 || style.mode == 3) {
|
||||
const bool gradient = style.mode == 2;
|
||||
field(menu, gradient ? "Colors" : "Pick", gradient ? "The two colors it moves between. Click one to change it."
|
||||
: "Click the color to change it.");
|
||||
ImGui::BeginDisabled(!shown);
|
||||
constexpr auto flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoAlpha;
|
||||
if (ImGui::ColorEdit3("##from", picked.from.data(), flags)) picked.changed = true;
|
||||
// Beside the swatch, what it gives: the one colour, or all that lies between the two.
|
||||
ImGui::SameLine(0, px(6));
|
||||
const auto at = ImGui::GetCursorScreenPos();
|
||||
const float swatch = gradient ? ImGui::GetFrameHeight() + px(6) : 0.0f;
|
||||
const ImVec2 bar(std::max(px(20), ImGui::GetContentRegionAvail().x - swatch), ImGui::GetFrameHeight());
|
||||
const auto solid = [&](const std::array<float, 3> &colour) {
|
||||
return ImGui::ColorConvertFloat4ToU32(ImVec4(colour[0], colour[1], colour[2], shown ? 1.0f : .4f));
|
||||
};
|
||||
const auto left = solid(picked.from), right = solid(gradient ? picked.to : picked.from);
|
||||
ImGui::GetWindowDrawList()->AddRectFilledMultiColor(at, ImVec2(at.x + bar.x, at.y + bar.y), left, right, right, left);
|
||||
ImGui::Dummy(bar);
|
||||
if (gradient) {
|
||||
ImGui::SameLine(0, px(6));
|
||||
if (ImGui::ColorEdit3("##to", picked.to.data(), flags)) picked.changed = true;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
// One save when the mouse lets go, not one for every shade dragged through.
|
||||
if (picked.changed && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
picked.changed = false;
|
||||
picked.waiting = 120;
|
||||
send(style.mode, style.speed);
|
||||
}
|
||||
}
|
||||
// A colour that stands still has no speed.
|
||||
if (style.mode == 0 || style.mode == 2) {
|
||||
int speed = style.speed;
|
||||
field(menu, "Speed");
|
||||
if (choice(menu, "speed", speed, {"NORMAL", "SLOW", "FAST"}, shown)) send(style.mode, speed);
|
||||
}
|
||||
end_card();
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
void multiplayer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
|
||||
|
||||
Reference in new issue
Block a user