#[cfg(test)]
#[path = "../tests/tf2/voting.rs"]
mod tests;
use crate::interfaces::game_event::GameEvent;
use crate::{Game, Server};
use sdk_raw::tf2::voting::{DEDICATED_SERVER, IssueVtables, MAX_VOTE_OPTIONS};
use sdk_raw::util;
use std::ffi::{CStr, CString, c_int, c_void};
use std::marker::PhantomData;
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VoteChoice(u8);
impl VoteChoice {
pub const NO: Self = Self(1);
pub const YES: Self = Self(0);
pub const fn new(index: u8) -> Option<Self> {
if (index as usize) < MAX_VOTE_OPTIONS {
Some(Self(index))
} else {
None
}
}
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VoteDecision {
Allow,
Block,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VoteEvent {
#[doc(alias("vote_options"))]
Started {
vote_id: c_int,
options: Vec<CString>,
},
#[doc(alias("vote_cast"))]
Cast {
vote_id: c_int,
entity_index: u8,
choice: VoteChoice,
team: c_int,
},
}
impl VoteEvent {
pub const EVENTS: [&'static CStr; 2] = [c"vote_options", c"vote_cast"];
pub fn from_event(event: GameEvent<'_>) -> Result<Option<Self>, VoteEventError> {
decode(
event.name(),
|key| event.get_int(key),
|key| event.get_string(key),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("TF2 vote event has a missing or invalid `{0}` field")]
pub struct VoteEventError(
pub &'static str,
);
#[derive(Debug, thiserror::Error)]
pub enum VoteHookTargetError {
#[error("built-in voting hooks require Team Fortress 2")]
WrongGame,
#[error("the game module could not be inspected")]
Image(#[from] std::io::Error),
#[error("the game module has an unsupported executable image")]
InvalidImage,
#[error("no unique primary vtable found for TF2 vote issue {0:?}")]
UnsupportedIssue(VoteIssue),
}
impl From<util::Error> for VoteHookTargetError {
fn from(error: util::Error) -> Self {
match error {
util::Error::InvalidImage => Self::InvalidImage,
util::Error::Io(error) => Self::Image(error),
}
}
}
#[doc(alias("CBaseIssue"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VoteIssue {
#[doc(alias("CRestartGameIssue"))]
RestartGame,
#[doc(alias("CKickIssue"))]
Kick,
#[doc(alias("CChangeLevelIssue"))]
ChangeLevel,
#[doc(alias("CNextLevelIssue"))]
NextLevel,
#[doc(alias("CExtendLevelIssue"))]
ExtendLevel,
#[doc(alias("CScrambleTeams"))]
ScrambleTeams,
#[doc(alias("CMannVsMachineChangeChallengeIssue"))]
ChangeMission,
#[doc(alias("CEnableTemporaryHalloweenIssue"))]
Eternaween,
#[doc(alias("CTeamAutoBalanceIssue"))]
TeamAutoBalance,
#[doc(alias("CClassLimitsIssue"))]
ClassLimits,
#[doc(alias("CPauseGameIssue"))]
PauseGame,
}
impl VoteIssue {
pub const ALL: [Self; 11] = [
Self::RestartGame,
Self::Kick,
Self::ChangeLevel,
Self::NextLevel,
Self::ExtendLevel,
Self::ScrambleTeams,
Self::ChangeMission,
Self::Eternaween,
Self::TeamAutoBalance,
Self::ClassLimits,
Self::PauseGame,
];
fn class(self) -> &'static str {
match self {
Self::RestartGame => "CRestartGameIssue",
Self::Kick => "CKickIssue",
Self::ChangeLevel => "CChangeLevelIssue",
Self::NextLevel => "CNextLevelIssue",
Self::ExtendLevel => "CExtendLevelIssue",
Self::ScrambleTeams => "CScrambleTeams",
Self::ChangeMission => "CMannVsMachineChangeChallengeIssue",
Self::Eternaween => "CEnableTemporaryHalloweenIssue",
Self::TeamAutoBalance => "CTeamAutoBalanceIssue",
Self::ClassLimits => "CClassLimitsIssue",
Self::PauseGame => "CPauseGameIssue",
}
}
pub const fn name(self) -> &'static str {
match self {
Self::RestartGame => "RestartGame",
Self::Kick => "Kick",
Self::ChangeLevel => "ChangeLevel",
Self::NextLevel => "NextLevel",
Self::ExtendLevel => "ExtendLevel",
Self::ScrambleTeams => "ScrambleTeams",
Self::ChangeMission => "ChangeMission",
Self::Eternaween => "Eternaween",
Self::TeamAutoBalance => "TeamAutoBalance",
Self::ClassLimits => "ClassLimits",
Self::PauseGame => "PauseGame",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct VoteIssueVtable<'s> {
pub issue: VoteIssue,
vtable: NonNull<*mut c_void>,
_scope: PhantomData<&'s Server<'s>>,
}
impl VoteIssueVtable<'_> {
pub const fn as_ptr(self) -> NonNull<*mut c_void> {
self.vtable
}
}
#[derive(Debug, Clone, Copy)]
pub struct VoteRequest<'a> {
pub issue: VoteIssue,
pub caller_entity_index: c_int,
pub details: &'a CStr,
}
impl VoteRequest<'_> {
pub const fn is_server_request(self) -> bool {
self.caller_entity_index == DEDICATED_SERVER
}
}
pub trait VoteStartHandler: 'static {
#[doc(alias("RequestCallVote"))]
fn vote_start(&self, server: Server<'_>, request: VoteRequest<'_>) -> VoteDecision;
}
fn decode(
name: &CStr,
mut integer: impl FnMut(&CStr) -> Option<c_int>,
mut string: impl FnMut(&CStr) -> Option<CString>,
) -> Result<Option<VoteEvent>, VoteEventError> {
if !VoteEvent::EVENTS.contains(&name) {
return Ok(None);
}
let vote_id = integer(c"voteidx").ok_or(VoteEventError("voteidx"))?;
if name == c"vote_options" {
let keys: [&CStr; MAX_VOTE_OPTIONS] =
[c"option1", c"option2", c"option3", c"option4", c"option5"];
let count = integer(c"count")
.and_then(|count| usize::try_from(count).ok())
.filter(|count| (2..=keys.len()).contains(count))
.ok_or(VoteEventError("count"))?;
let options = keys[..count]
.iter()
.map(|key| string(key).ok_or(VoteEventError("option")))
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(VoteEvent::Started { vote_id, options }))
} else {
let entity_index = integer(c"entityid")
.and_then(|index| u8::try_from(index).ok())
.filter(|index| *index != 0)
.ok_or(VoteEventError("entityid"))?;
let choice = integer(c"vote_option")
.and_then(|index| u8::try_from(index).ok())
.and_then(VoteChoice::new)
.ok_or(VoteEventError("vote_option"))?;
let team = integer(c"team").ok_or(VoteEventError("team"))?;
Ok(Some(VoteEvent::Cast {
vote_id,
entity_index,
choice,
team,
}))
}
}
pub fn vote_issue_vtables(
server: Server<'_>,
) -> Result<Vec<VoteIssueVtable<'_>>, VoteHookTargetError> {
if server.game() != Game::TeamFortress2 {
return Err(VoteHookTargetError::WrongGame);
}
let vtables = unsafe { IssueVtables::load(server.game_server_factory().as_raw()) }?;
VoteIssue::ALL
.into_iter()
.map(|issue| {
let pointer = vtables
.find(issue.class())
.ok_or(VoteHookTargetError::UnsupportedIssue(issue))?;
Ok(VoteIssueVtable {
issue,
vtable: pointer,
_scope: PhantomData,
})
})
.collect()
}