#include "throwdown_wire.h" #include #include #include #include namespace dingosdk::multiplayer { namespace { bool individual(std::uint64_t id) noexcept { return (id >> 56) == 1 && ((id >> 52) & 15) == 1 && (id & 0xffffffffULL); } bool valid_series(const std::string &series) noexcept { return !series.empty() && series.size() <= max_throwdown_series && std::all_of(series.begin(), series.end(), [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; }); } bool valid_challenge_id(const std::string &id) noexcept { return !id.empty() && id.size() <= max_challenge_id && std::all_of(id.begin(), id.end(), [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.'; }); } struct Writer { std::vector bytes; void integer(std::uint64_t value, unsigned size) { for (unsigned i = 0; i < size; ++i) bytes.push_back(static_cast(value >> (8 * i))); } void blob(const std::vector &value, unsigned size) { integer(value.size(), size); bytes.insert(bytes.end(), value.begin(), value.end()); } }; struct Reader { std::span bytes; std::size_t at{}; bool ok = true; std::uint64_t integer(unsigned size) { if (!ok || size > bytes.size() - at) { ok = false; return 0; } std::uint64_t value{}; for (unsigned i = 0; i < size; ++i) value |= std::uint64_t{bytes[at + i]} << (8 * i); at += size; return value; } std::vector blob(unsigned size, std::size_t limit) { const auto length = integer(size); if (!ok || length > limit || length > bytes.size() - at) { ok = false; return {}; } std::vector value(bytes.begin() + static_cast(at), bytes.begin() + static_cast(at + length)); at += static_cast(length); return value; } }; } // namespace bool valid_throwdown(const ThrowdownMessage &m) noexcept { if (!individual(m.leader) || !m.id) return false; switch (m.kind) { case ThrowdownMessage::Kind::offer: return valid_series(m.series) && !m.placement.empty() && m.placement.size() <= max_throwdown_params && m.settings.size() <= max_throwdown_params && m.order.size() <= max_throwdown_order && std::all_of(m.order.begin(), m.order.end(), individual); case ThrowdownMessage::Kind::start: return !m.order.empty() && m.order.size() <= max_throwdown_order && std::all_of(m.order.begin(), m.order.end(), individual); case ThrowdownMessage::Kind::row: return m.board < max_throwdown_boards; case ThrowdownMessage::Kind::attempt: case ThrowdownMessage::Kind::turn_end: return m.value > 0; case ThrowdownMessage::Kind::close: case ThrowdownMessage::Kind::join: case ThrowdownMessage::Kind::leave: case ThrowdownMessage::Kind::score: return true; case ThrowdownMessage::Kind::challenge_start: return valid_series(m.series) && valid_challenge_id(m.challenge) && m.order.size() >= 2 && m.order.size() <= max_challenge_players && m.order.front() == m.leader && std::all_of(m.order.begin(), m.order.end(), individual); case ThrowdownMessage::Kind::challenge_attempt: return !m.criteria.empty() && m.criteria.size() % challenge_criteria_size == 0 && m.criteria.size() <= max_challenge_criteria * challenge_criteria_size && m.indexes.size() % 4 == 0 && m.indexes.size() <= max_challenge_criteria * 4; case ThrowdownMessage::Kind::challenge_optout: case ThrowdownMessage::Kind::challenge_slam: case ThrowdownMessage::Kind::challenge_leave: return true; case ThrowdownMessage::Kind::beacon: return m.leader && std::all_of(m.location.begin(), m.location.end(), [](float v) { return std::isfinite(v) && std::abs(v) < 1e6f; }); } return false; } std::vector encode_throwdown(const ThrowdownMessage &m) { if (!valid_throwdown(m)) throw std::invalid_argument("Invalid throwdown message"); Writer w; w.integer(static_cast(m.kind), 1); w.integer(m.leader, 8); w.integer(m.id, 4); switch (m.kind) { case ThrowdownMessage::Kind::offer: w.integer(m.series.size(), 1); w.bytes.insert(w.bytes.end(), m.series.begin(), m.series.end()); w.blob(m.placement, 2); w.blob(m.settings, 2); [[fallthrough]]; case ThrowdownMessage::Kind::start: w.integer(m.order.size(), 1); for (const auto id : m.order) w.integer(id, 8); break; case ThrowdownMessage::Kind::score: case ThrowdownMessage::Kind::turn_end: w.integer(static_cast(m.value), 4); break; case ThrowdownMessage::Kind::row: w.integer(m.board, 1); w.integer(m.add ? 1 : 0, 1); w.integer(static_cast(m.value), 4); break; case ThrowdownMessage::Kind::attempt: w.integer(static_cast(m.value), 4); w.integer(m.add ? 1 : 0, 1); w.bytes.insert(w.bytes.end(), m.trick.begin(), m.trick.end()); break; case ThrowdownMessage::Kind::challenge_start: w.integer(m.series.size(), 1); w.bytes.insert(w.bytes.end(), m.series.begin(), m.series.end()); w.integer(m.challenge.size(), 1); w.bytes.insert(w.bytes.end(), m.challenge.begin(), m.challenge.end()); w.integer(m.add ? 1 : 0, 1); w.integer(m.order.size(), 1); for (const auto id : m.order) w.integer(id, 8); break; case ThrowdownMessage::Kind::challenge_attempt: w.blob(m.criteria, 2); w.blob(m.indexes, 2); break; case ThrowdownMessage::Kind::challenge_slam: w.integer(static_cast(m.value), 4); break; case ThrowdownMessage::Kind::beacon: w.integer(m.add ? 1 : 0, 1); if (m.add) for (const auto v : m.location) w.integer(std::bit_cast(v), 4); break; default: break; } return std::move(w.bytes); } std::optional decode_throwdown(std::span bytes) noexcept { try { Reader r{bytes}; ThrowdownMessage m; const auto kind = r.integer(1); if (kind < 1 || kind > 15) return {}; m.kind = static_cast(kind); m.leader = r.integer(8); m.id = static_cast(r.integer(4)); switch (m.kind) { case ThrowdownMessage::Kind::offer: { const auto length = r.integer(1); if (!r.ok || length > max_throwdown_series || length > bytes.size() - r.at) return {}; m.series.assign(reinterpret_cast(bytes.data() + r.at), static_cast(length)); r.at += static_cast(length); m.placement = r.blob(2, max_throwdown_params); m.settings = r.blob(2, max_throwdown_params); [[fallthrough]]; } case ThrowdownMessage::Kind::start: { const auto count = r.integer(1); if (!r.ok || count > max_throwdown_order) return {}; for (std::uint64_t i = 0; i < count; ++i) m.order.push_back(r.integer(8)); break; } case ThrowdownMessage::Kind::score: case ThrowdownMessage::Kind::turn_end: m.value = static_cast(r.integer(4)); break; case ThrowdownMessage::Kind::row: { m.board = static_cast(r.integer(1)); const auto add = r.integer(1); if (add > 1) return {}; m.add = add != 0; m.value = static_cast(r.integer(4)); break; } case ThrowdownMessage::Kind::attempt: { m.value = static_cast(r.integer(4)); const auto landed = r.integer(1); if (landed > 1) return {}; m.add = landed != 0; for (auto &byte : m.trick) byte = static_cast(r.integer(1)); break; } case ThrowdownMessage::Kind::challenge_start: { const auto text = [&](std::size_t limit, std::string &out) { const auto length = r.integer(1); if (!r.ok || length > limit || length > bytes.size() - r.at) return false; out.assign(reinterpret_cast(bytes.data() + r.at), static_cast(length)); r.at += static_cast(length); return true; }; if (!text(max_throwdown_series, m.series) || !text(max_challenge_id, m.challenge)) return {}; const auto contest = r.integer(1); if (contest > 1) return {}; m.add = contest != 0; const auto count = r.integer(1); if (!r.ok || count > max_challenge_players) return {}; for (std::uint64_t i = 0; i < count; ++i) m.order.push_back(r.integer(8)); break; } case ThrowdownMessage::Kind::challenge_attempt: m.criteria = r.blob(2, max_challenge_criteria * challenge_criteria_size); m.indexes = r.blob(2, max_challenge_criteria * 4); break; case ThrowdownMessage::Kind::challenge_slam: m.value = static_cast(r.integer(4)); break; case ThrowdownMessage::Kind::beacon: { const auto placed = r.integer(1); if (placed > 1) return {}; m.add = placed != 0; if (m.add) for (auto &v : m.location) v = std::bit_cast(static_cast(r.integer(4))); break; } default: break; } if (!r.ok || r.at != bytes.size() || !valid_throwdown(m)) return {}; return m; } catch (...) { return {}; } } } // namespace dingosdk::multiplayer