#[cfg(test)]
#[path = "tests/vote_hooks.rs"]
mod tests;
use crate::MetamodApi;
use crate::hook::{
Handler, HookAction, HookCall, HookError, HookId, HookTarget, HookTiming, VirtualFunction,
};
use source_sdk_2013::raw::tf2::voting::{
REQUEST_CALL_VOTE_SLOT, RequestCallVoteFn as RequestCallVote,
};
use source_sdk_2013::tf2::voting::{
VoteDecision, VoteHookTargetError, VoteIssue, VoteRequest, VoteStartHandler, vote_issue_vtables,
};
use source_sdk_2013::{Server, ServerBinding, sys};
use std::cell::Cell;
use std::ffi::{CStr, c_int};
use std::marker::PhantomData;
use std::rc::Rc;
const REQUEST: VirtualFunction<RequestCallVote> = VirtualFunction::new(REQUEST_CALL_VOTE_SLOT);
static ROUTES: [VoteRoute; 11] = [
VoteRoute::new(VoteIssue::RestartGame),
VoteRoute::new(VoteIssue::Kick),
VoteRoute::new(VoteIssue::ChangeLevel),
VoteRoute::new(VoteIssue::NextLevel),
VoteRoute::new(VoteIssue::ExtendLevel),
VoteRoute::new(VoteIssue::ScrambleTeams),
VoteRoute::new(VoteIssue::ChangeMission),
VoteRoute::new(VoteIssue::Eternaween),
VoteRoute::new(VoteIssue::TeamAutoBalance),
VoteRoute::new(VoteIssue::ClassLimits),
VoteRoute::new(VoteIssue::PauseGame),
];
#[derive(Debug, thiserror::Error)]
pub enum VoteHookError {
#[error(transparent)]
Target(#[from] VoteHookTargetError),
#[error(transparent)]
Hook(#[from] HookError),
}
#[must_use = "retain vote hooks to support explicitly removing them"]
pub struct VoteHooks {
hooks: Vec<HookId>,
_not_thread_safe: PhantomData<Rc<()>>,
}
impl VoteHooks {
pub fn remove(self, api: MetamodApi<'_>) {
for hook in self.hooks {
api.remove_hook(hook);
for route in &ROUTES {
if route.state.get().is_some_and(|state| state.0 == hook) {
route.state.set(None);
}
}
}
}
}
struct VoteRoute {
issue: VoteIssue,
state: Cell<Option<(HookId, ServerBinding, &'static dyn VoteStartHandler)>>,
}
impl VoteRoute {
const fn new(issue: VoteIssue) -> Self {
Self {
issue,
state: Cell::new(None),
}
}
}
impl Handler<RequestCallVote> for VoteRoute {
fn call(&self, call: &HookCall<'_, RequestCallVote>) -> HookAction<bool> {
if call.superseded() == Some(true) {
return HookAction::Ignore;
}
let Some((_, binding, handler)) = self.state.get() else {
return HookAction::Ignore;
};
let (caller_entity_index, details, failure, time) = call.args();
if call.this().is_null() || details.is_null() || failure.is_null() || time.is_null() {
return HookAction::Ignore;
}
let scope = ();
let server = unsafe { binding.server(&scope) };
let details = unsafe { CStr::from_ptr(details) };
let request = VoteRequest {
issue: self.issue,
caller_entity_index,
details,
};
unsafe { dispatch(server, request, handler, failure, time) }
}
}
unsafe impl Sync for VoteRoute {}
impl MetamodApi<'_> {
pub fn hook_vote_starts(
self,
server: Server<'_>,
binding: ServerBinding,
handler: &'static dyn VoteStartHandler,
) -> Result<VoteHooks, VoteHookError> {
if ROUTES.iter().any(|route| {
route
.state
.get()
.is_some_and(|state| self.has_hook(state.0))
}) {
return Err(HookError::AlreadyInstalled.into());
}
let targets = vote_issue_vtables(server)?;
let mut installed = VoteHooks {
hooks: Vec::with_capacity(targets.len()),
_not_thread_safe: PhantomData,
};
for target in targets {
let route = ROUTES
.iter()
.find(|route| route.issue == target.issue)
.expect("every built-in vote issue has a route");
match unsafe {
self.add_hook(
REQUEST,
HookTarget::vtable(target.as_ptr()),
HookTiming::Pre,
route,
)
} {
Ok(hook) => {
route.state.set(Some((hook, binding, handler)));
installed.hooks.push(hook);
}
Err(error) => {
installed.remove(self);
return Err(error.into());
}
}
}
Ok(installed)
}
}
unsafe fn dispatch(
server: Server<'_>,
request: VoteRequest<'_>,
handler: &dyn VoteStartHandler,
failure: *mut sys::vote_create_failed_t,
time: *mut c_int,
) -> HookAction<bool> {
match handler.vote_start(server, request) {
VoteDecision::Allow => HookAction::Ignore,
VoteDecision::Block => {
unsafe {
failure.write(sys::vote_create_failed_t_VOTE_FAILED_GENERIC);
time.write(-1);
}
HookAction::Supersede(false)
}
}
}