#[cfg(test)]
#[path = "../../tests/net/cheats.rs"]
mod tests;
use crate::bitbuf::BitWriter;
use crate::commands::CommandFlags;
use crate::interfaces::cvar::ConVar;
use crate::interfaces::{Cvar, GameClient};
use crate::net::incoming::{Incoming, IncomingKind, IncomingMessage, Verdict};
use crate::net::messages::{GetCvarValue, MAX_CONVAR_LEN, SetConVar, StringCmd};
use crate::net::{EncodeError, NetChannel, NetMessage, Reliability, SendError};
use crate::players::UserId;
use crate::server::{InterfaceError, Server};
use sdk_raw::interfaces::plugin_helpers::QUERY_CVAR_VALUE_INTACT;
use sdk_raw::net::incoming::MAX_SET_CONVARS;
use std::ffi::{CStr, CString, c_int};
use std::hash::{BuildHasher, RandomState};
use std::mem::take;
use std::num::NonZero;
use std::ops::RangeInclusive;
use std::time::{Duration, Instant};
const CHEATS: &CStr = c"sv_cheats";
pub const COOKIES: RangeInclusive<c_int> = -0x4348_FFFF..=-0x4348_0000;
const MAX_RETIRED: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Begun {
Lease(LeaseId),
ServerCheats,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CheatsError {
#[error("the engine has no game server")]
NoServer,
#[error("no connected client has user ID {0}")]
NoClient(UserId),
#[error("bots and SourceTV have no client-side sv_cheats")]
FakeClient,
#[error("the client has no net channel")]
NoChannel,
#[error("the client shares the server's process and its sv_cheats")]
Loopback,
#[error("the server does not register sv_cheats")]
MissingCheats,
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error(transparent)]
Send(#[from] SendError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheatsEvent {
Finished {
user_id: UserId,
lease: LeaseId,
outcome: LeaseOutcome,
},
Retrying {
user_id: UserId,
lease: LeaseId,
attempt: u8,
},
RestoreFailed {
user_id: UserId,
error: SendError,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CheatsOptions {
pub confirm_timeout: Duration,
pub max_attempts: NonZero<u8>,
pub retry_delay: Duration,
}
impl CheatsOptions {
pub const DEFAULT: Self = Self {
confirm_timeout: Duration::from_secs(3),
max_attempts: NonZero::new(3).unwrap(),
retry_delay: Duration::from_secs(1),
};
fn retry_at(self, now: Instant, failures: u8) -> Instant {
let delay = self
.retry_delay
.saturating_mul(1 << failures.saturating_sub(1).min(16));
now.checked_add(delay).unwrap_or(now)
}
}
impl Default for CheatsOptions {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug)]
struct Client {
confirmed: bool,
leases: Vec<Lease>,
refused: bool,
retired: Vec<c_int>,
spoofed: bool,
user_id: UserId,
window: Window,
}
impl Client {
const fn new(user_id: UserId) -> Self {
Self {
confirmed: false,
leases: Vec::new(),
refused: false,
retired: Vec::new(),
spoofed: false,
user_id,
window: Window::Closed,
}
}
fn invalidate(&mut self) {
let mut cookies = Vec::new();
for lease in &mut self.leases {
if let LeaseState::Answered { cookie, .. } | LeaseState::Sent { cookie, .. } =
lease.state
{
cookies.push(cookie);
lease.state = LeaseState::Queued { not_before: None };
}
}
for cookie in cookies {
self.retire(cookie);
}
}
fn is_blank(&self) -> bool {
self.is_idle()
&& !self.confirmed
&& !self.refused
&& !self.spoofed
&& self.retired.is_empty()
}
fn is_idle(&self) -> bool {
self.leases.is_empty() && self.window == Window::Closed
}
fn restore<L: Link>(
&mut self,
link: &mut L,
peer: L::Peer,
events: &mut Vec<CheatsEvent>,
) -> bool {
let (whole, cheats) = {
let values = link.restore_values();
(
restore_bits(values),
restore_bits(values.get(..1).unwrap_or_default()),
)
};
let error = match link.send(peer, &whole) {
Ok(()) => {
self.window = Window::Closed;
return true;
}
Err(error) => error,
};
events.push(CheatsEvent::RestoreFailed {
user_id: self.user_id,
error,
});
if self.window == Window::Open && link.send(peer, &cheats).is_ok() {
self.window = Window::Resync;
return true;
}
false
}
fn restore_due(&self, now: Instant, timeout: Duration) -> bool {
match self.window {
Window::Closed => false,
Window::Resync => true,
Window::Open => {
self.leases.iter().all(|lease| !lease.is_sent())
|| self
.leases
.iter()
.any(|lease| lease.timed_out(now, timeout))
}
}
}
fn retire(&mut self, cookie: c_int) {
if self.retired.len() >= MAX_RETIRED {
self.retired.remove(0);
}
self.retired.push(cookie);
}
fn settle(
&mut self,
now: Instant,
options: CheatsOptions,
events: &mut Vec<CheatsEvent>,
cancel: bool,
) {
let user_id = self.user_id;
for mut lease in take(&mut self.leases) {
let outcome = match lease.state {
LeaseState::Answered { cookie, cheats } => {
self.retire(cookie);
match cheats {
true => {
self.confirmed = true;
LeaseOutcome::Confirmed
}
false => {
self.refused = true;
LeaseOutcome::Refused
}
}
}
LeaseState::Unverified { cookie } => {
self.retire(cookie);
LeaseOutcome::Unverified
}
LeaseState::Queued { .. } if cancel => LeaseOutcome::Cancelled,
LeaseState::Queued { .. } => {
self.leases.push(lease);
continue;
}
LeaseState::Sent { cookie, .. } if cancel => {
self.retire(cookie);
LeaseOutcome::Cancelled
}
LeaseState::Sent { cookie, .. }
if !lease.timed_out(now, options.confirm_timeout) =>
{
self.retire(cookie);
lease.state = LeaseState::Queued { not_before: None };
self.leases.push(lease);
continue;
}
LeaseState::Sent { cookie, .. } => {
self.retire(cookie);
lease.failures = lease.failures.saturating_add(1);
if lease.failures >= options.max_attempts.get() {
LeaseOutcome::TimedOut
} else {
lease.requeue(now, options, user_id, events);
self.leases.push(lease);
continue;
}
}
};
events.push(CheatsEvent::Finished {
user_id,
lease: lease.id,
outcome,
});
}
}
}
#[doc(alias("sv_cheats"))]
#[derive(Debug)]
pub struct ClientCheats {
clients: Vec<Client>,
next_cookie: Option<c_int>,
next_lease: NonZero<u64>,
options: CheatsOptions,
server_cheats: bool,
}
impl ClientCheats {
pub const fn new(options: CheatsOptions) -> Self {
Self {
clients: Vec::new(),
next_cookie: None,
next_lease: NonZero::<u64>::MIN,
options,
server_cheats: false,
}
}
fn awaits_answers(&self) -> bool {
self.clients.iter().any(|client| {
!client.retired.is_empty() || client.leases.iter().any(|lease| lease.cookie().is_some())
})
}
pub fn begin(
&mut self,
server: Server<'_>,
user_id: UserId,
purpose: Purpose,
) -> Result<Begun, CheatsError> {
let mut link = EngineLink::new(server)?;
let client = link.client(user_id).ok_or(CheatsError::NoClient(user_id))?;
if client.is_fake() || client.is_hltv() {
return Err(CheatsError::FakeClient);
}
if client
.net_channel()
.ok_or(CheatsError::NoChannel)?
.is_loopback()
{
return Err(CheatsError::Loopback);
}
self.begin_with(&mut link, user_id, purpose, Instant::now())
}
fn begin_with<L: Link>(
&mut self,
link: &mut L,
user_id: UserId,
purpose: Purpose,
now: Instant,
) -> Result<Begun, CheatsError> {
let (peer, active) = link.peer(user_id).ok_or(CheatsError::NoClient(user_id))?;
let commands = commands_bits(&purpose).map_err(SendError::from)?;
let server_cheats = link.server_cheats();
self.see_server_cheats(server_cheats);
if server_cheats {
if !commands.is_empty() {
link.send(peer, &commands)?;
}
return Ok(Begun::ServerCheats);
}
let id = self.next_lease();
let index = self.entry(user_id);
let mut lease = Lease {
failures: 0,
id,
purpose,
state: LeaseState::Queued { not_before: None },
};
if active && self.clients[index].window != Window::Resync {
let cookie = self.next_cookie();
let sent = spoof_bits([(cookie, &lease.purpose)])
.map_err(SendError::from)
.and_then(|bits| link.send(peer, &bits));
if let Err(error) = sent {
if self.clients[index].is_blank() {
self.clients.remove(index);
}
return Err(error.into());
}
let client = &mut self.clients[index];
lease.state = LeaseState::Sent { cookie, at: now };
client.spoofed = true;
client.window = Window::Open;
}
self.clients[index].leases.push(lease);
Ok(Begun::Lease(id))
}
pub fn clear(&mut self) {
self.clients.retain_mut(|client| {
let resync = client.spoofed || client.window != Window::Closed;
*client = Client {
window: Window::Resync,
..Client::new(client.user_id)
};
resync
});
}
pub fn client(&self, user_id: UserId) -> Option<ClientStatus> {
self.find(user_id).map(|client| ClientStatus {
confirmed: client.confirmed,
pending: client.leases.len(),
refused: client.refused,
restore_pending: client.window != Window::Closed,
})
}
fn entry(&mut self, user_id: UserId) -> usize {
if let Some(index) = self
.clients
.iter()
.position(|client| client.user_id == user_id)
{
return index;
}
self.clients.push(Client::new(user_id));
self.clients.len() - 1
}
fn find(&self, user_id: UserId) -> Option<&Client> {
self.clients.iter().find(|client| client.user_id == user_id)
}
pub fn forget(&mut self, user_id: UserId) -> bool {
let known = self.clients.len();
self.clients.retain(|client| client.user_id != user_id);
self.clients.len() != known
}
pub fn lease(&self, lease: LeaseId) -> Option<LeaseStatus> {
self.clients
.iter()
.flat_map(|client| &client.leases)
.find(|candidate| candidate.id == lease)
.map(|lease| match lease.state {
LeaseState::Answered { .. } | LeaseState::Unverified { .. } => {
LeaseStatus::Answered
}
LeaseState::Queued { .. } => LeaseStatus::Queued,
LeaseState::Sent { .. } => LeaseStatus::Sent,
})
}
fn next_cookie(&mut self) -> c_int {
let span = COOKIES.end() - COOKIES.start() + 1;
let mut next = self.next_cookie.unwrap_or_else(|| {
(RandomState::new().hash_one(span) % span.unsigned_abs() as u64) as c_int
});
for _ in 0..span {
let cookie = COOKIES.end() - next;
next = (next + 1) % span;
let in_use = self.clients.iter().any(|client| {
client.retired.contains(&cookie)
|| client
.leases
.iter()
.any(|lease| lease.cookie() == Some(cookie))
});
if !in_use {
self.next_cookie = Some(next);
return cookie;
}
}
self.next_cookie = Some(next);
*COOKIES.end()
}
fn next_lease(&mut self) -> LeaseId {
let id = LeaseId(self.next_lease);
self.next_lease = self.next_lease.saturating_add(1);
id
}
pub fn on_game_frame(&mut self, server: Server<'_>) -> Result<Vec<CheatsEvent>, CheatsError> {
if self.clients.iter().all(Client::is_idle) {
return Ok(Vec::new());
}
Ok(self.on_game_frame_with(&mut EngineLink::new(server)?, Instant::now()))
}
fn on_game_frame_with<L: Link>(&mut self, link: &mut L, now: Instant) -> Vec<CheatsEvent> {
let mut events = Vec::new();
let mut index = 0;
self.see_server_cheats(link.server_cheats());
while index < self.clients.len() {
let Some((peer, active)) = link.peer(self.clients[index].user_id) else {
let mut client = self.clients.remove(index);
client.settle(now, self.options, &mut events, true);
continue;
};
if active {
let options = self.options;
let client = &mut self.clients[index];
if client.restore_due(now, options.confirm_timeout)
&& client.restore(link, peer, &mut events)
{
client.settle(now, options, &mut events, false);
}
self.send_waiting(link, index, peer, now, &mut events);
}
index += 1;
}
events
}
#[doc(alias("ProcessRespondCvarValue"))]
pub fn on_incoming(&mut self, message: IncomingMessage<'_>) -> Verdict {
if message.kind() != IncomingKind::RespondCvarValue || !self.awaits_answers() {
return Verdict::Continue;
}
let Some(user_id) = message.client().user_id() else {
return Verdict::Continue;
};
match message.decode() {
Some(response) => self.on_response(user_id, &response),
None => {
self.on_unreadable(user_id);
Verdict::Continue
}
}
}
#[doc(alias("CLC_RespondCvarValue"))]
pub fn on_response(&mut self, user_id: UserId, response: &Incoming) -> Verdict {
let Incoming::RespondCvarValue {
cookie,
status,
name,
value,
} = response
else {
return Verdict::Continue;
};
if !COOKIES.contains(cookie) || !name.as_bytes().eq_ignore_ascii_case(CHEATS.to_bytes()) {
return Verdict::Continue;
}
let Some(client) = self
.clients
.iter_mut()
.find(|client| client.user_id == user_id)
else {
return Verdict::Continue;
};
let lease = client
.leases
.iter_mut()
.find(|lease| lease.cookie() == Some(*cookie));
match lease {
Some(lease) if lease.is_sent() => {
let cheats = *status != QUERY_CVAR_VALUE_INTACT || is_set(value);
lease.state = LeaseState::Answered {
cookie: *cookie,
cheats,
};
Verdict::Block
}
Some(_) => Verdict::Block,
None if client.retired.contains(cookie) => Verdict::Block,
None => Verdict::Continue,
}
}
fn on_unreadable(&mut self, user_id: UserId) {
let Some(client) = self
.clients
.iter_mut()
.find(|client| client.user_id == user_id)
else {
return;
};
for lease in &mut client.leases {
if let LeaseState::Sent { cookie, .. } = lease.state {
lease.state = LeaseState::Unverified { cookie };
}
}
}
pub const fn options(&self) -> CheatsOptions {
self.options
}
pub fn restore_all(&mut self, server: Server<'_>) -> Result<Vec<CheatsEvent>, CheatsError> {
if self.clients.iter().all(Client::is_idle) {
return Ok(Vec::new());
}
Ok(self.restore_all_with(&mut EngineLink::new(server)?, Instant::now()))
}
fn restore_all_with<L: Link>(&mut self, link: &mut L, now: Instant) -> Vec<CheatsEvent> {
let mut events = Vec::new();
let options = self.options;
self.clients.retain_mut(|client| {
let peer = link.peer(client.user_id);
if let Some((peer, _)) = peer
&& client.window != Window::Closed
{
client.restore(link, peer, &mut events);
}
client.settle(now, options, &mut events, true);
peer.is_some()
});
events
}
fn see_server_cheats(&mut self, set: bool) {
if self.server_cheats && !set {
for client in &mut self.clients {
client.invalidate();
}
}
self.server_cheats = set;
}
fn send_waiting<L: Link>(
&mut self,
link: &mut L,
index: usize,
peer: L::Peer,
now: Instant,
events: &mut Vec<CheatsEvent>,
) {
let client = &self.clients[index];
let due = client
.leases
.iter()
.filter(|lease| lease.is_due(now))
.count();
if due == 0 || client.window == Window::Resync {
return;
}
let options = self.options;
if link.server_cheats() {
let client = &mut self.clients[index];
for lease in take(&mut client.leases) {
if !lease.is_due(now) {
client.leases.push(lease);
continue;
}
let sent = commands_bits(&lease.purpose)
.map_err(SendError::from)
.and_then(|bits| match bits.is_empty() {
true => Ok(()),
false => link.send(peer, &bits),
});
events.push(CheatsEvent::Finished {
user_id: client.user_id,
lease: lease.id,
outcome: match sent {
Ok(()) => LeaseOutcome::ServerCheats,
Err(error) => LeaseOutcome::SendFailed(error),
},
});
}
return;
}
let cookies: Vec<c_int> = (0..due).map(|_| self.next_cookie()).collect();
let client = &mut self.clients[index];
let sent = spoof_bits(
client
.leases
.iter()
.filter(|lease| lease.is_due(now))
.zip(cookies.iter().copied())
.map(|(lease, cookie)| (cookie, &lease.purpose)),
)
.map_err(SendError::from)
.and_then(|bits| link.send(peer, &bits));
match sent {
Ok(()) => {
let waiting = client.leases.iter_mut().filter(|lease| lease.is_due(now));
for (lease, cookie) in waiting.zip(cookies) {
lease.state = LeaseState::Sent { cookie, at: now };
}
client.spoofed = true;
client.window = Window::Open;
}
Err(error) => {
let user_id = client.user_id;
for mut lease in take(&mut client.leases) {
if !lease.is_due(now) {
client.leases.push(lease);
continue;
}
lease.failures = lease.failures.saturating_add(1);
if lease.failures >= options.max_attempts.get() {
events.push(CheatsEvent::Finished {
user_id,
lease: lease.id,
outcome: LeaseOutcome::SendFailed(error.clone()),
});
} else {
lease.requeue(now, options, user_id, events);
client.leases.push(lease);
}
}
}
}
}
}
impl Default for ClientCheats {
fn default() -> Self {
Self::new(CheatsOptions::DEFAULT)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClientStatus {
pub confirmed: bool,
pub pending: usize,
pub refused: bool,
pub restore_pending: bool,
}
struct EngineLink<'s> {
cheats: ConVar<'s>,
clients: Vec<(UserId, GameClient<'s>)>,
cvar: Cvar<'s>,
restore: Option<Vec<(CString, CString)>>,
}
impl<'s> EngineLink<'s> {
fn new(server: Server<'s>) -> Result<Self, CheatsError> {
let game_server = server
.valve_engine()?
.game_server()
.ok_or(CheatsError::NoServer)?;
let cvar = server.cvar()?;
let cheats = cvar.find_var(CHEATS).ok_or(CheatsError::MissingCheats)?;
let clients = game_server
.clients()
.filter(|client| client.is_connected())
.filter_map(|client| Some((client.user_id()?, client)))
.collect();
Ok(Self {
cheats,
clients,
cvar,
restore: None,
})
}
fn client(&self, user_id: UserId) -> Option<GameClient<'s>> {
self.clients
.iter()
.find(|&&(id, _)| id == user_id)
.map(|&(_, client)| client)
}
}
impl<'s> Link for EngineLink<'s> {
type Peer = NetChannel<'s>;
fn peer(&mut self, user_id: UserId) -> Option<(Self::Peer, bool)> {
let client = self.client(user_id)?;
Some((client.net_channel()?, client.is_active()))
}
fn restore_values(&mut self) -> &[(CString, CString)] {
let (cvar, cheats) = (self.cvar, self.cheats);
self.restore
.get_or_insert_with(|| restore_values_of(cvar, cheats))
}
fn send(&mut self, peer: Self::Peer, bits: &BitWriter) -> Result<(), SendError> {
peer.send_encoded(bits, Reliability::Reliable)
}
fn server_cheats(&mut self) -> bool {
self.cheats.int() != 0
}
}
#[derive(Debug)]
struct Lease {
failures: u8,
id: LeaseId,
purpose: Purpose,
state: LeaseState,
}
impl Lease {
const fn cookie(&self) -> Option<c_int> {
match self.state {
LeaseState::Answered { cookie, .. }
| LeaseState::Sent { cookie, .. }
| LeaseState::Unverified { cookie } => Some(cookie),
LeaseState::Queued { .. } => None,
}
}
fn is_due(&self, now: Instant) -> bool {
matches!(self.state, LeaseState::Queued { not_before } if not_before.is_none_or(|at| now >= at))
}
const fn is_sent(&self) -> bool {
matches!(self.state, LeaseState::Sent { .. })
}
fn requeue(
&mut self,
now: Instant,
options: CheatsOptions,
user_id: UserId,
events: &mut Vec<CheatsEvent>,
) {
self.state = LeaseState::Queued {
not_before: Some(options.retry_at(now, self.failures)),
};
events.push(CheatsEvent::Retrying {
user_id,
lease: self.id,
attempt: self.failures.saturating_add(1),
});
}
fn timed_out(&self, now: Instant, timeout: Duration) -> bool {
matches!(self.state, LeaseState::Sent { at, .. } if now.saturating_duration_since(at) >= timeout)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LeaseId(NonZero<u64>);
impl LeaseId {
pub const fn get(self) -> u64 {
self.0.get()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LeaseOutcome {
Confirmed,
ServerCheats,
Refused,
TimedOut,
Unverified,
SendFailed(SendError),
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LeaseState {
Answered { cookie: c_int, cheats: bool },
Queued { not_before: Option<Instant> },
Sent { cookie: c_int, at: Instant },
Unverified { cookie: c_int },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LeaseStatus {
Queued,
Sent,
Answered,
}
trait Link {
type Peer: Copy;
fn peer(&mut self, user_id: UserId) -> Option<(Self::Peer, bool)>;
fn restore_values(&mut self) -> &[(CString, CString)];
fn send(&mut self, peer: Self::Peer, bits: &BitWriter) -> Result<(), SendError>;
fn server_cheats(&mut self) -> bool;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub enum Purpose {
#[default]
Observe,
Commands(Vec<CString>),
}
impl Purpose {
pub fn commands(&self) -> &[CString] {
match self {
Self::Observe => &[],
Self::Commands(commands) => commands,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Window {
Closed,
Open,
Resync,
}
fn commands_bits(purpose: &Purpose) -> Result<BitWriter, EncodeError> {
let mut bits = BitWriter::new();
for command in purpose.commands() {
bits.write_bits(&StringCmd { command }.encode()?);
}
Ok(bits)
}
fn fits(name: &CStr, value: &CStr) -> bool {
name.count_bytes() <= MAX_CONVAR_LEN && value.count_bytes() <= MAX_CONVAR_LEN
}
fn is_set(value: &CStr) -> bool {
value
.to_str()
.ok()
.and_then(|value| value.trim().parse::<f64>().ok())
.is_some_and(|value| value.trunc() != 0.0)
}
fn restore_bits(values: &[(CString, CString)]) -> BitWriter {
let values: Vec<(&CStr, &CStr)> = values
.iter()
.filter(|(name, value)| fits(name, value))
.map(|(name, value)| (name.as_c_str(), value.as_c_str()))
.collect();
let mut bits = BitWriter::new();
for chunk in values.chunks(MAX_SET_CONVARS) {
if let Ok(message) = (SetConVar { convars: chunk }).encode() {
bits.write_bits(&message);
}
}
bits
}
#[doc(alias("FCVAR_REPLICATED"))]
pub fn restore_values(cvar: Cvar<'_>) -> Result<Vec<(CString, CString)>, CheatsError> {
let cheats = cvar.find_var(CHEATS).ok_or(CheatsError::MissingCheats)?;
Ok(restore_values_of(cvar, cheats))
}
fn restore_values_of(cvar: Cvar<'_>, cheats: ConVar<'_>) -> Vec<(CString, CString)> {
let mut value = cheats.string();
if value.count_bytes() > MAX_CONVAR_LEN {
value = CString::new(cheats.int().to_string()).unwrap_or_default();
}
let mut values = vec![(CHEATS.to_owned(), value)];
let replicated_cheat = CommandFlags::REPLICATED | CommandFlags::CHEAT;
for var in cvar.vars() {
if !var.flags().contains(replicated_cheat) || var.is_default() {
continue;
}
let name = var.name();
let listed = values
.iter()
.any(|(listed, _)| listed.as_bytes().eq_ignore_ascii_case(name.to_bytes()));
if listed {
continue;
}
let value = var.string();
if fits(name, &value) {
values.push((name.to_owned(), value));
}
}
values
}
fn spoof_bits<'a>(
leases: impl IntoIterator<Item = (c_int, &'a Purpose)>,
) -> Result<BitWriter, EncodeError> {
let mut bits = SetConVar {
convars: &[(CHEATS, c"1")],
}
.encode()?;
for (cookie, purpose) in leases {
bits.write_bits(&commands_bits(purpose)?);
bits.write_bits(
&GetCvarValue {
cookie,
name: CHEATS,
}
.encode()?,
);
}
Ok(bits)
}