mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-27 13:07:07 +01:00
183 lines
8.9 KiB
C++
183 lines
8.9 KiB
C++
#pragma once
|
|
#include "Extension/Multiplayer/Net/protocol.h"
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
using MemberSlots = std::array<Member, max_remote_players>;
|
|
|
|
// Preserve slot identity for surviving players, including across joins/leaves.
|
|
// Callers retire native resources whenever a slot's id or epoch changes.
|
|
inline MemberSlots roster_slots(const MemberSlots &previous, std::span<const Member> members,
|
|
std::uint64_t local) {
|
|
MemberSlots next{};
|
|
for (std::size_t i = 0; i < previous.size(); ++i)
|
|
for (const auto &m : members)
|
|
if (m.id != local && m.id == previous[i].id)
|
|
next[i] = m;
|
|
for (const auto &m : members) {
|
|
if (m.id == local ||
|
|
std::any_of(next.begin(), next.end(), [&](const auto &v) { return v.id == m.id; }))
|
|
continue;
|
|
const auto free = std::find_if(next.begin(), next.end(), [](const auto &v) { return !v.id; });
|
|
if (free == next.end())
|
|
throw std::runtime_error("Session roster exceeds capacity.");
|
|
*free = m;
|
|
}
|
|
return next;
|
|
}
|
|
inline bool trusted_roster(const Packet &p, std::uint64_t host, std::uint64_t host_epoch, std::uint64_t local,
|
|
std::uint64_t local_epoch) noexcept {
|
|
if (p.kind != PacketKind::roster || p.source != host || p.epoch != host_epoch ||
|
|
!valid_roster(p.members, p.capacity) || !p.distances.valid() || !valid_multiplayer_tps(p.tps))
|
|
return false;
|
|
bool found_host{}, found_local{};
|
|
for (const auto &m : p.members) {
|
|
if (m.id == host)
|
|
found_host = m.epoch == host_epoch;
|
|
if (m.id == local)
|
|
found_local = m.epoch == local_epoch;
|
|
}
|
|
return found_host && found_local;
|
|
}
|
|
inline bool routed_source(const Packet &p, const Member &member, std::uint64_t connection, bool hosting,
|
|
std::uint64_t host, bool direct = false) noexcept {
|
|
// Only the host may forward another Steam identity. A guest never chooses
|
|
// its authority by putting a different sender ID in a packet.
|
|
return member.id && member.epoch && p.source == member.id && p.epoch == member.epoch &&
|
|
(hosting || direct ? connection == member.id : connection == host);
|
|
}
|
|
// A route report is a short lease: missing reports or a source reconnect restore relay.
|
|
inline bool needs_relay(std::span<const Member> routes, std::uint64_t reported, const Member &source,
|
|
std::uint64_t now) noexcept {
|
|
return !reported || now < reported || now - reported > 1500000 ||
|
|
std::none_of(routes.begin(), routes.end(),
|
|
[&](const auto &m) { return m.id == source.id && m.epoch == source.epoch; });
|
|
}
|
|
inline bool dial_peer(std::uint64_t local, std::uint64_t remote, std::uint64_t host) noexcept {
|
|
return local && remote && remote != host && local != host && local < remote;
|
|
}
|
|
// Recent world positions only; uncertainty returns to full rate. Separate
|
|
// enter/exit thresholds avoid oscillating at a distance boundary.
|
|
inline std::uint32_t pose_interval(float distance_squared, std::uint32_t previous,
|
|
const MultiplayerDistances &settings = {}, unsigned tps = 20) noexcept {
|
|
const auto full = multiplayer_pose_interval(tps);
|
|
if (!std::isfinite(distance_squared) || distance_squared < 0) return full;
|
|
if (!settings.valid()) return full;
|
|
const auto beyond = [&](int metres) { return distance_squared > static_cast<float>(metres * metres); };
|
|
if (!beyond(settings.full_rate_return)) return full;
|
|
if (beyond(settings.low_rate_start)) return 200000;
|
|
if (previous == 200000 && beyond(settings.half_rate_return)) return 200000;
|
|
if (beyond(settings.half_rate_start) || previous == 100000 || previous == 200000) return 100000;
|
|
return full;
|
|
}
|
|
struct PoseDelivery {
|
|
std::uint64_t source{}, epoch{}, last_sent{};
|
|
std::uint32_t interval_us = 50000;
|
|
std::uint64_t next_source_time{};
|
|
};
|
|
// Preserve the timer phase across variable client frames/packet arrivals. Only
|
|
// the newest state is sent; elapsed slots are skipped without catch-up packets.
|
|
inline void advance_pose_deadline(std::uint64_t &next, std::uint64_t now, std::uint64_t interval) {
|
|
next = now + interval - (next && now >= next ? (now - next) % interval : 0);
|
|
}
|
|
// Reduce optional direct uploads when a guest cannot sustain fan-out. The host
|
|
// remains its first destination, and other receivers restore relay through leases.
|
|
struct DirectUploadBudget {
|
|
unsigned limit = max_remote_players - 1;
|
|
std::uint64_t next_check{}, recover_at{}, skipped{};
|
|
void update(std::uint64_t now, std::uint64_t queue_us, std::uint64_t skipped_total, unsigned active) {
|
|
if (now < next_check)
|
|
return;
|
|
next_check = now + 1000000;
|
|
const auto new_skips = skipped_total >= skipped ? skipped_total - skipped : 0;
|
|
skipped = skipped_total;
|
|
if (active && (queue_us > 50000 || new_skips > 5)) {
|
|
limit = std::min(limit, active);
|
|
limit /= 2;
|
|
recover_at = now + 10000000;
|
|
} else if (now >= recover_at && queue_us < 10000 && !new_skips) {
|
|
limit = std::min<unsigned>(max_remote_players - 1, limit + 1);
|
|
recover_at = now + 5000000;
|
|
}
|
|
}
|
|
};
|
|
// What one player's game sends of each relayed stream, with room to spare: a host passes on
|
|
// no more than this from any one source, whatever the connection's overall budget allows.
|
|
// Outfits: captured twice a second and sent when changed. Counted over five seconds.
|
|
inline constexpr unsigned outfit_burst = 12;
|
|
struct OutfitBudget {
|
|
std::uint64_t since{};
|
|
unsigned count{};
|
|
bool accept(std::uint64_t now) noexcept {
|
|
if (now < since || now - since >= 5000000) { since = now; count = 0; }
|
|
return ++count <= outfit_burst;
|
|
}
|
|
};
|
|
// Skater sound: at most one packet per network tick, a few samples each.
|
|
struct SoundBudget {
|
|
std::uint64_t since{};
|
|
std::size_t packets{}, samples{};
|
|
bool accept(std::uint64_t now, std::size_t count) noexcept {
|
|
if (now < since || now - since >= 1000000) { since = now; packets = samples = 0; }
|
|
if (packets >= multiplayer_tick_rates.back() + 30U || samples + count > 400) return false;
|
|
++packets;
|
|
samples += count;
|
|
return true;
|
|
}
|
|
};
|
|
// A connection that never finished joining (it timed out, or had the wrong password or code)
|
|
// may try again at once the first time. After that every failure makes its Steam ID wait
|
|
// longer before a host takes its connection again, so one account cannot hold a player slot
|
|
// or try passwords over and over.
|
|
struct JoinBackoff {
|
|
struct Entry {
|
|
std::uint64_t until{}, last{};
|
|
unsigned failures{};
|
|
};
|
|
static constexpr std::uint64_t forget_us = 30ULL * 60 * 1000000, longest_us = 10ULL * 60 * 1000000;
|
|
std::map<std::uint64_t, Entry> entries;
|
|
// Notes a failed attempt and returns how many this ID has made lately.
|
|
unsigned failed(std::uint64_t id, std::uint64_t now) {
|
|
auto &entry = entries[id];
|
|
if (entry.last && now >= entry.last && now - entry.last > forget_us) entry.failures = 0;
|
|
++entry.failures;
|
|
entry.last = now;
|
|
const auto wait = entry.failures < 2 ? 0ULL
|
|
: std::min<std::uint64_t>(longest_us, 5000000ULL << std::min(entry.failures - 2, 8U));
|
|
entry.until = now + wait;
|
|
return entry.failures;
|
|
}
|
|
bool waiting(std::uint64_t id, std::uint64_t now) const {
|
|
const auto found = entries.find(id);
|
|
return found != entries.end() && now < found->second.until;
|
|
}
|
|
void joined(std::uint64_t id) { entries.erase(id); }
|
|
void prune(std::uint64_t now) {
|
|
std::erase_if(entries, [&](const auto &entry) {
|
|
return now >= entry.second.last && now - entry.second.last > forget_us;
|
|
});
|
|
}
|
|
};
|
|
// How much one peer may send in a second. `when` is when the packet arrived (the transport's
|
|
// TransportMessage::arrived), not when it is read: a receiver that could not read for a few
|
|
// seconds (a server whose console held it up, a game loading a map) reads everything that
|
|
// arrived meanwhile at once, and counted by reading time every peer looked like a flood and
|
|
// was dropped. Packets are not always read in arrival order (Steam's lanes are read one after
|
|
// another): one from before the second being counted is counted in it.
|
|
struct ReceiveBudget {
|
|
std::uint64_t since{}, bytes{}, packets{};
|
|
bool accept(std::uint64_t when, std::size_t size, unsigned sources = 1) {
|
|
if (when > since && when - since >= 1000000) {
|
|
since = when;
|
|
bytes = packets = 0;
|
|
}
|
|
bytes += size;
|
|
++packets;
|
|
return sources && sources <= max_remote_players && bytes <= 2ULL * 1024 * 1024 * sources &&
|
|
packets <= (2ULL * multiplayer_tick_rates.back() + 80 + 32) * sources;
|
|
}
|
|
};
|
|
} // namespace dingosdk::multiplayer
|