use crate::Game;
use crate::datatables::{NetProp, NetPropError};
use crate::entities::Entity;
use crate::server::{InterfaceError, Server};
use crate::tf2::game_rules::{GameRules, GameRulesError};
use crate::tf2::player::TfPlayer;
use sdk_raw::tf2::ammo as raw;
use std::ffi::c_int;
use std::ptr::NonNull;
pub const MAX_GIVEN: c_int = 1 << 20;
#[derive(Debug, thiserror::Error)]
pub enum AmmoError {
#[error("reserve ammo requires a TF2 server and a CTFPlayer entity")]
NotTfPlayer,
#[error("cannot give {0} reserve ammo at once, only from 0 to {MAX_GIVEN}")]
InvalidCount(c_int),
#[error("cannot hold {0} reserve ammo, less than none")]
InvalidReserve(c_int),
#[error("the player holds {0} of the ammo type, less than none")]
NegativeReserve(c_int),
#[error(transparent)]
GameRules(#[from] GameRulesError),
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error(transparent)]
NetProp(#[from] NetPropError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AmmoType {
#[doc(alias("TF_AMMO_PRIMARY"))]
Primary,
#[doc(alias("TF_AMMO_SECONDARY"))]
Secondary,
#[doc(alias("TF_AMMO_METAL"))]
Metal,
#[doc(alias("TF_AMMO_GRENADES1"))]
Grenades1,
#[doc(alias("TF_AMMO_GRENADES2"))]
Grenades2,
#[doc(alias("TF_AMMO_GRENADES3"))]
Grenades3,
}
impl AmmoType {
pub const fn from_raw(index: c_int) -> Option<Self> {
match index {
raw::AMMO_PRIMARY => Some(Self::Primary),
raw::AMMO_SECONDARY => Some(Self::Secondary),
raw::AMMO_METAL => Some(Self::Metal),
raw::AMMO_GRENADES1 => Some(Self::Grenades1),
raw::AMMO_GRENADES2 => Some(Self::Grenades2),
raw::AMMO_GRENADES3 => Some(Self::Grenades3),
_ => None,
}
}
pub const fn to_raw(self) -> c_int {
match self {
Self::Primary => raw::AMMO_PRIMARY,
Self::Secondary => raw::AMMO_SECONDARY,
Self::Metal => raw::AMMO_METAL,
Self::Grenades1 => raw::AMMO_GRENADES1,
Self::Grenades2 => raw::AMMO_GRENADES2,
Self::Grenades3 => raw::AMMO_GRENADES3,
}
}
}
fn check_player(server: Server<'_>, player: Entity<'_>) -> Result<(), AmmoError> {
if server.game() != Game::TeamFortress2
|| !player
.server_class()
.is_some_and(|class| class.name() == c"CTFPlayer")
{
return Err(AmmoError::NotTfPlayer);
}
Ok(())
}
#[doc(alias("GiveAmmo"))]
pub fn give_ammo(
server: Server<'_>,
player: Entity<'_>,
ammo_type: AmmoType,
count: c_int,
suppress_sound: bool,
) -> Result<c_int, AmmoError> {
check_player(server, player)?;
if !(0..=MAX_GIVEN).contains(&count) {
return Err(AmmoError::InvalidCount(count));
}
let held = reserve_prop(server, player, ammo_type)?.get::<c_int>(player)?;
if held < 0 {
return Err(AmmoError::NegativeReserve(held));
}
Ok(unsafe { raw::give_ammo(player.as_ptr(), count, ammo_type.to_raw(), suppress_sound) })
}
fn lowest_without_room<E>(mut room: impl FnMut(c_int) -> Result<bool, E>) -> Result<c_int, E> {
let mut low = 0;
let mut high = 0;
while room(high)? {
if high == c_int::MAX {
return Ok(c_int::MAX);
}
low = high + 1;
high = high.saturating_mul(2).max(1);
}
while low < high {
let middle = low + (high - low) / 2;
if room(middle)? {
low = middle + 1;
} else {
high = middle;
}
}
Ok(high)
}
#[doc(alias("GetMaxAmmo", "max_ammo"))]
pub fn max_reserve(
server: Server<'_>,
player: Entity<'_>,
ammo_type: AmmoType,
) -> Result<c_int, AmmoError> {
check_player(server, player)?;
let tf_player = TfPlayer::new(server, player).map_err(|_| AmmoError::NotTfPlayer)?;
let rules = GameRules::get(server)?;
let prop = reserve_prop(server, player, ammo_type)?;
let held = prop.get::<c_int>(player)?;
let base = unsafe { NonNull::new_unchecked(player.as_ptr()) }.cast();
let max = lowest_without_room(|count| {
unsafe { prop.set_at(base, count) }?;
Ok::<_, AmmoError>(rules.can_have_ammo(tf_player, ammo_type))
});
unsafe { prop.set_at(base, held) }?;
max
}
#[doc(alias("GetAmmoCount", "m_iAmmo"))]
pub fn reserve(
server: Server<'_>,
player: Entity<'_>,
ammo_type: AmmoType,
) -> Result<c_int, AmmoError> {
check_player(server, player)?;
Ok(reserve_prop(server, player, ammo_type)?.get::<c_int>(player)?)
}
fn reserve_prop<'s>(
server: Server<'s>,
player: Entity<'s>,
ammo_type: AmmoType,
) -> Result<NetProp<'s>, AmmoError> {
Ok(server
.server_game_dll()?
.entity_net_prop(player, c"m_iAmmo")?
.element(ammo_type.to_raw() as usize)?)
}
#[doc(alias("SetAmmoCount", "m_iAmmo"))]
pub fn set_reserve(
server: Server<'_>,
player: Entity<'_>,
ammo_type: AmmoType,
count: c_int,
) -> Result<(), AmmoError> {
check_player(server, player)?;
if count < 0 {
return Err(AmmoError::InvalidReserve(count));
}
let engine = server.valve_engine()?;
unsafe { reserve_prop(server, player, ammo_type)?.set(engine, player, count) }?;
Ok(())
}