mod capture_area;
mod control_points;
mod flags;
mod objective_resource;
mod round_timer;
mod team_spawn;
mod train_watcher;
mod zones;
use crate::datatables::{NetProp, NetPropError, NetVar, Storage};
use crate::entities::Entity;
use crate::inputs::{InputError, InputValue};
use crate::tf2::game_rules::GameRules;
use crate::tf2::round_end::RoundEndError;
use crate::{Game, InterfaceError, Server};
use std::ffi::{CStr, CString, c_int};
pub use capture_area::CaptureArea;
pub use control_points::{CaptureWins, ControlPoint, ControlPointMaster, ControlPointRound};
pub use flags::{CaptureFlag, CaptureZone, FlagStatus, FlagType};
pub use objective_resource::ObjectiveResource;
pub use round_timer::{KothLogic, RoundTimer, RoundTimerOutput, RoundWin, TimerState, WinReason};
pub use team_spawn::{TeamSpawn, TeamSpawnMode};
pub use train_watcher::TrainWatcher;
pub use zones::{NoBuildZone, RegenerateZone, RespawnRoom, RespawnRoomVisualizer};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Objective<'s> {
server: Server<'s>,
entity: Entity<'s>,
}
impl<'s> Objective<'s> {
pub(crate) fn new(
server: Server<'s>,
entity: Entity<'s>,
class: &CStr,
expected: &'static str,
) -> Result<Self, ObjectiveError> {
if server.game() != Game::TeamFortress2 || !entity.has_data_map_class(class) {
return Err(ObjectiveError::WrongClass { expected });
}
Ok(Self { server, entity })
}
pub(crate) fn bool_field(
self,
class: &'static CStr,
field: &'static CStr,
) -> Result<bool, ObjectiveError> {
let offset = self.field_offset(class, field, &[sys::_fieldtypes_FIELD_BOOLEAN], 1, 0)?;
Ok(unsafe { self.entity.as_ptr().byte_add(offset).cast::<u8>().read() } != 0)
}
pub(crate) fn check_live(self) -> Result<(), ObjectiveError> {
if self.entity.is_marked_for_deletion() {
Err(ObjectiveError::MarkedForDeletion)
} else {
Ok(())
}
}
pub(crate) fn check_round_end(self) -> Result<(), ObjectiveError> {
let tools = self.server.server_tools()?;
let rules = GameRules::get(self.server).map_err(RoundEndError::from)?;
Ok(rules.check_round_end(tools)?)
}
pub(crate) fn current_time(self) -> Result<f32, ObjectiveError> {
self.server
.player_info_manager()?
.global_vars()
.map(|globals| globals.current_time())
.ok_or(ObjectiveError::NoGlobals)
}
pub(crate) fn element<T: NetVar>(self, name: &CStr, index: usize) -> Result<T, ObjectiveError> {
Ok(self.net_prop(name)?.element(index)?.get(self.entity)?)
}
pub(crate) const fn entity(self) -> Entity<'s> {
self.entity
}
fn field_offset(
self,
class: &'static CStr,
field: &'static CStr,
types: &[sys::fieldtype_t],
size: usize,
index: usize,
) -> Result<usize, ObjectiveError> {
self.check_live()?;
self.entity
.data_maps()
.find(|map| map.class_name() == Some(class))
.and_then(|map| {
map.fields().iter().find(|declared| {
declared.name() == Some(field) && types.contains(&declared.fieldType)
})
})
.filter(|declared| {
let count = usize::from(declared.fieldSize);
index < count
&& usize::try_from(declared.fieldSizeInBytes).ok() == size.checked_mul(count)
})
.and_then(|declared| declared.offset()?.checked_add(index.checked_mul(size)?))
.filter(|offset| {
offset.is_multiple_of(size)
&& offset.checked_add(size).is_some_and(|end| end <= 65_536)
})
.ok_or(ObjectiveError::UnsupportedLayout { class, field })
}
pub(crate) fn flag(self, name: &CStr) -> Result<bool, ObjectiveError> {
Ok(flag(self.net_prop(name)?, self.entity)?)
}
pub(crate) fn flag_element(self, name: &CStr, index: usize) -> Result<bool, ObjectiveError> {
Ok(flag(self.net_prop(name)?.element(index)?, self.entity)?)
}
pub(crate) fn float_element(
self,
class: &'static CStr,
field: &'static CStr,
index: usize,
) -> Result<f32, ObjectiveError> {
let offset = self.field_offset(
class,
field,
&[sys::_fieldtypes_FIELD_FLOAT, sys::_fieldtypes_FIELD_TIME],
size_of::<f32>(),
index,
)?;
Ok(unsafe { self.entity.as_ptr().byte_add(offset).cast::<f32>().read() })
}
pub(crate) fn float_field(
self,
class: &'static CStr,
field: &'static CStr,
) -> Result<f32, ObjectiveError> {
self.float_element(class, field, 0)
}
pub(crate) fn get<T: NetVar>(self, name: &CStr) -> Result<T, ObjectiveError> {
Ok(self.net_prop(name)?.get(self.entity)?)
}
pub(crate) fn handle_entity(self, name: &CStr) -> Result<Option<Entity<'s>>, ObjectiveError> {
let handle = self.net_prop(name)?.get_handle(self.entity)?;
Ok(self.server.server_tools()?.entity_by_handle(handle))
}
pub(crate) fn input(self, name: &CStr, value: InputValue<'_>) -> Result<(), ObjectiveError> {
self.input_from(name, value, self.entity, self.entity)
}
pub(crate) fn input_from(
self,
name: &CStr,
value: InputValue<'_>,
activator: Entity<'_>,
caller: Entity<'_>,
) -> Result<(), ObjectiveError> {
let tools = self.server.server_tools()?;
Ok(tools.accept_input(self.entity, name, value, activator, caller)?)
}
pub(crate) fn int_field(
self,
class: &'static CStr,
field: &'static CStr,
) -> Result<c_int, ObjectiveError> {
let offset = self.field_offset(
class,
field,
&[sys::_fieldtypes_FIELD_INTEGER],
size_of::<c_int>(),
0,
)?;
Ok(unsafe { self.entity.as_ptr().byte_add(offset).cast::<c_int>().read() })
}
pub(crate) fn key_value(self, key: &CStr) -> Result<Option<CString>, ObjectiveError> {
self.check_live()?;
Ok(self.server.server_tools()?.key_value(self.entity, key))
}
pub(crate) fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, ObjectiveError> {
self.check_live()?;
Ok(self
.server
.server_game_dll()?
.entity_net_prop(self.entity, name)?)
}
pub(crate) const fn server(self) -> Server<'s> {
self.server
}
pub(crate) fn set_key_value(
self,
key: &'static CStr,
value: &CStr,
) -> Result<(), ObjectiveError> {
self.check_live()?;
if self
.server
.server_tools()?
.set_key_value(self.entity, key, value)
{
Ok(())
} else {
Err(ObjectiveError::KeyValueRejected { key })
}
}
pub(crate) fn string_key(
self,
class: &'static CStr,
key: &'static CStr,
) -> Result<CString, ObjectiveError> {
self.key_value(key)?
.ok_or(ObjectiveError::UnsupportedLayout { class, field: key })
}
}
#[derive(Debug, thiserror::Error)]
pub enum ObjectiveError {
#[error("the engine's globals are unavailable")]
NoGlobals,
#[error(transparent)]
Input(#[from] InputError),
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the entity refused the key value {key:?}")]
KeyValueRejected {
key: &'static CStr,
},
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error(transparent)]
RoundEnd(#[from] RoundEndError),
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("control point {index} is out of range")]
PointOutOfRange {
index: usize,
},
#[error("{name:?} holds {value}, which the SDK does not know")]
UnknownValue {
name: &'static CStr,
value: c_int,
},
#[error("the data description of {class:?} lacks a usable {field:?}")]
UnsupportedLayout {
class: &'static CStr,
field: &'static CStr,
},
#[error("the entity is not a TF2 {expected}")]
WrongClass {
expected: &'static str,
},
}
pub(crate) fn find_by_class<'s, T>(
server: Server<'s>,
class_name: &CStr,
wrap: impl Fn(Server<'s>, Entity<'s>) -> Result<T, ObjectiveError>,
) -> Result<Option<T>, ObjectiveError> {
let tools = server.server_tools()?;
let mut found = tools.find_by_class_name(None, class_name);
while let Some(entity) = found {
if !entity.is_marked_for_deletion()
&& let Ok(wrapped) = wrap(server, entity)
{
return Ok(Some(wrapped));
}
found = tools.find_by_class_name(Some(entity), class_name);
}
Ok(None)
}
pub(crate) fn find_by_name<'s, T>(
server: Server<'s>,
name: &CStr,
wrap: impl Fn(Server<'s>, Entity<'s>) -> Result<T, ObjectiveError>,
) -> Result<Option<T>, ObjectiveError> {
let tools = server.server_tools()?;
let mut found = tools.find_by_name(None, name);
while let Some(entity) = found {
if !entity.is_marked_for_deletion()
&& let Ok(wrapped) = wrap(server, entity)
{
return Ok(Some(wrapped));
}
found = tools.find_by_name(Some(entity), name);
}
Ok(None)
}
pub(crate) fn flag(prop: NetProp<'_>, entity: Entity<'_>) -> Result<bool, NetPropError> {
match prop.storage() {
Storage::I32 | Storage::U32 => Ok(prop.get::<i32>(entity)? != 0),
_ => Ok(prop.get::<u8>(entity)? != 0),
}
}