#[cfg(test)]
#[path = "../tests/tf2/loadout.rs"]
mod tests;
use crate::entities::{Entity, EntityHandle};
use crate::interfaces::GameClient;
use crate::interfaces::game_event::GameEvent;
use crate::players::UserId;
use crate::tf2::attributes::{AttributeSet, SchemaToken};
use crate::tf2::game_events::GameEventId;
use crate::tf2::weapons::{IntoWeaponSlot, ItemDefinitionIndex, PlayerWeapons, WeaponError};
use crate::tf2::wearables::{MAX_NETWORKED_WEARABLES, PlayerWearables, Wearable, WearableError};
use crate::{Game, InterfaceError, Server};
use std::cell::{Cell, RefCell};
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{CStr, c_int};
const USER_ID_KEY: &CStr = c"userid";
#[derive(Debug)]
struct ApplyGuard<'r>(&'r LoadoutReapplier);
impl Drop for ApplyGuard<'_> {
fn drop(&mut self) {
self.0.applying.set(false);
self.0.deferred.take();
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ApplyReport {
pub user_id: UserId,
pub wearables: Vec<(ItemDefinitionIndex, Result<WearableOutcome, WearableError>)>,
pub weapons: Vec<(c_int, Result<WeaponOutcome, WeaponError>)>,
pub deferred: Vec<UserId>,
}
impl ApplyReport {
pub fn is_ok(&self) -> bool {
self.wearables.iter().all(|(_, outcome)| outcome.is_ok())
&& self.weapons.iter().all(|(_, outcome)| outcome.is_ok())
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Loadout {
weapons: BTreeMap<c_int, AttributeSet>,
wearables: Vec<ItemDefinitionIndex>,
}
impl Loadout {
pub const fn new() -> Self {
Self {
weapons: BTreeMap::new(),
wearables: Vec::new(),
}
}
pub fn insert_weapon_attributes(
&mut self,
slot: impl IntoWeaponSlot,
attributes: AttributeSet,
) -> Option<AttributeSet> {
let slot = slot.into_weapon_slot();
if attributes.is_empty() {
return self.weapons.remove(&slot);
}
self.weapons.insert(slot, attributes)
}
pub fn insert_wearable(
&mut self,
definition: ItemDefinitionIndex,
) -> Result<bool, LoadoutError> {
if self.wearables.contains(&definition) {
return Ok(false);
}
if self.wearables.len() >= MAX_NETWORKED_WEARABLES {
return Err(LoadoutError::TooManyWearables);
}
self.wearables.push(definition);
Ok(true)
}
pub fn is_empty(&self) -> bool {
self.weapons.is_empty() && self.wearables.is_empty()
}
pub fn remove_weapon_attributes(&mut self, slot: impl IntoWeaponSlot) -> Option<AttributeSet> {
self.weapons.remove(&slot.into_weapon_slot())
}
pub fn remove_wearable(&mut self, definition: ItemDefinitionIndex) -> bool {
let len = self.wearables.len();
self.wearables.retain(|&wanted| wanted != definition);
self.wearables.len() != len
}
pub fn weapon_attributes(&self, slot: impl IntoWeaponSlot) -> Option<&AttributeSet> {
self.weapons.get(&slot.into_weapon_slot())
}
pub fn weapons(&self) -> impl Iterator<Item = (c_int, &AttributeSet)> {
self.weapons
.iter()
.map(|(&slot, attributes)| (slot, attributes))
}
pub fn wearables(&self) -> &[ItemDefinitionIndex] {
&self.wearables
}
pub fn with_weapon_attributes(
mut self,
slot: impl IntoWeaponSlot,
attributes: AttributeSet,
) -> Self {
self.insert_weapon_attributes(slot, attributes);
self
}
pub fn with_wearable(mut self, definition: ItemDefinitionIndex) -> Result<Self, LoadoutError> {
self.insert_wearable(definition)?;
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LoadoutError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the event has no valid `userid`")]
InvalidUserId,
#[error("no connected client with the user id has a player")]
NoPlayer,
#[error("the engine has no game server")]
NoServer,
#[error("loadouts require a TF2 player")]
NotTfPlayer,
#[error("the reapplier is already applying a loadout")]
Reentrant,
#[error("the loadout already has as many wearables as the game networks")]
TooManyWearables,
}
#[doc(alias("Regenerate", "ValidateWearables"))]
#[derive(Debug, Default)]
pub struct LoadoutReapplier {
applying: Cell<bool>,
deferred: RefCell<Vec<UserId>>,
loadouts: BTreeMap<UserId, Loadout>,
}
impl LoadoutReapplier {
pub const fn new() -> Self {
Self {
applying: Cell::new(false),
deferred: RefCell::new(Vec::new()),
loadouts: BTreeMap::new(),
}
}
pub unsafe fn apply<'s>(
&self,
server: Server<'s>,
user_id: UserId,
token: SchemaToken<'s>,
) -> Result<ApplyReport, LoadoutError> {
let _applying = self.begin(user_id)?;
let mut report = ApplyReport {
user_id,
wearables: Vec::new(),
weapons: Vec::new(),
deferred: Vec::new(),
};
let Some(loadout) = self
.loadouts
.get(&user_id)
.filter(|loadout| !loadout.is_empty())
else {
return Ok(report);
};
let player = find_player(server, user_id)?;
let wearables =
PlayerWearables::new(server, player).map_err(|_| LoadoutError::NotTfPlayer)?;
let weapons = PlayerWeapons::new(server, player).map_err(|_| LoadoutError::NotTfPlayer)?;
for &definition in &loadout.wearables {
let outcome = unsafe { give_missing(wearables, definition) };
report.wearables.push((definition, outcome));
}
for (&slot, attributes) in &loadout.weapons {
report
.weapons
.push((slot, apply_attributes(token, weapons, slot, attributes)));
}
report.deferred = self.deferred.take();
Ok(report)
}
fn begin(&self, user_id: UserId) -> Result<ApplyGuard<'_>, LoadoutError> {
if self.applying.replace(true) {
let mut deferred = self.deferred.borrow_mut();
if !deferred.contains(&user_id) {
deferred.push(user_id);
}
return Err(LoadoutError::Reentrant);
}
Ok(ApplyGuard(self))
}
pub fn clear(&mut self) {
self.loadouts.clear();
}
pub fn forget(&mut self, user_id: UserId) -> Option<Loadout> {
self.loadouts.remove(&user_id)
}
pub fn get(&self, user_id: UserId) -> Option<&Loadout> {
self.loadouts.get(&user_id)
}
pub fn get_mut(&mut self, user_id: UserId) -> Option<&mut Loadout> {
self.loadouts.get_mut(&user_id)
}
pub fn is_applying(&self) -> bool {
self.applying.get()
}
pub fn iter(&self) -> impl Iterator<Item = (UserId, &Loadout)> {
self.loadouts
.iter()
.map(|(&user_id, loadout)| (user_id, loadout))
}
#[doc(alias("post_inventory_application"))]
pub unsafe fn on_game_event<'s>(
&self,
server: Server<'s>,
event: GameEvent<'_>,
token: SchemaToken<'s>,
) -> Result<Option<ApplyReport>, LoadoutError> {
if event.name() != GameEventId::PostInventoryApplication.name_cstr() {
return Ok(None);
}
let user_id = event
.get_int(USER_ID_KEY)
.and_then(|raw| UserId::from_raw(raw).ok())
.ok_or(LoadoutError::InvalidUserId)?;
if self.loadouts.get(&user_id).is_none_or(Loadout::is_empty) {
return Ok(None);
}
unsafe { self.apply(server, user_id, token) }.map(Some)
}
pub fn retain(&mut self, mut keep: impl FnMut(UserId, &mut Loadout) -> bool) {
self.loadouts
.retain(|&user_id, loadout| keep(user_id, loadout));
}
pub fn retain_connected(&mut self, server: Server<'_>) -> Result<(), LoadoutError> {
let connected: BTreeSet<UserId> = server
.valve_engine()?
.game_server()
.ok_or(LoadoutError::NoServer)?
.clients()
.filter(|client| client.is_connected())
.filter_map(GameClient::user_id)
.collect();
self.loadouts
.retain(|user_id, _| connected.contains(user_id));
Ok(())
}
pub fn set(&mut self, user_id: UserId, loadout: Loadout) -> Option<Loadout> {
self.loadouts.insert(user_id, loadout)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WeaponOutcome {
Applied(EntityHandle),
NoWeapon,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WearableOutcome {
AlreadyWorn(EntityHandle),
Given(EntityHandle),
}
fn apply_attributes<'s>(
token: SchemaToken<'s>,
weapons: PlayerWeapons<'s>,
slot: c_int,
attributes: &AttributeSet,
) -> Result<WeaponOutcome, WeaponError> {
let Some(weapon) = weapons.get_slot(slot)? else {
return Ok(WeaponOutcome::NoWeapon);
};
let item = weapon.attributes()?;
attributes.apply(token, item)?;
item.reapply_provision(token)?;
Ok(WeaponOutcome::Applied(weapon.entity().handle()))
}
fn find_player(server: Server<'_>, user_id: UserId) -> Result<Entity<'_>, LoadoutError> {
if server.game() != Game::TeamFortress2 {
return Err(LoadoutError::NotTfPlayer);
}
server
.valve_engine()?
.edict_of_user_id(user_id)
.and_then(|edict| edict.entity())
.ok_or(LoadoutError::NoPlayer)
}
fn find_worn<'s>(
wearables: PlayerWearables<'s>,
definition: ItemDefinitionIndex,
) -> Result<Option<Wearable<'s>>, WearableError> {
let list = wearables.list()?;
for wearable in list.iter() {
if wearable.entity().is_marked_for_deletion() || wearable.is_disguise()? {
continue;
}
if wearable.definition()? == Some(definition) {
return Ok(Some(wearable));
}
}
Ok(None)
}
unsafe fn give_missing(
wearables: PlayerWearables<'_>,
definition: ItemDefinitionIndex,
) -> Result<WearableOutcome, WearableError> {
if let Some(worn) = find_worn(wearables, definition)? {
return Ok(WearableOutcome::AlreadyWorn(worn.entity().handle()));
}
let given = unsafe { wearables.give(definition) }?;
Ok(WearableOutcome::Given(given.entity().handle()))
}