use crate::NotThreadSafe;
use crate::server::{InterfaceError, Server};
use sdk_raw::interfaces::network_string_tables::{INVALID_STRING_INDEX, UNKNOWN_STRING_LENGTH};
pub use sdk_raw::interfaces::network_string_tables::MAX_USER_DATA_LEN;
use sdk_raw::util::cstr::{borrow_cstr, copy_cstr};
use sdk_raw::vcall;
use std::ffi::{CStr, CString, c_int};
use std::marker::PhantomData;
use std::ptr::{self, NonNull};
pub const DOWNLOADABLES: &CStr = c"downloadables";
pub const INFO_PANEL: &CStr = c"InfoPanel";
pub const MODEL_PRECACHE: &CStr = c"modelprecache";
pub const PARTICLE_EFFECT_NAMES: &CStr = c"ParticleEffectNames";
pub const SOUND_PRECACHE: &CStr = c"soundprecache";
interface! {
#[doc(alias("INetworkStringTableContainer"))]
pub struct NetworkStringTables(sys::INetworkStringTableContainer) = Engine sdk_raw::interfaces::network_string_tables::VERSION;
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AddDownloadableError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the engine has no `downloadables` string table")]
MissingTable,
#[error("the `downloadables` string table refused the file")]
Refused,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("the string table refused the string")]
pub struct AddStringError;
#[doc(alias("INetworkStringTable"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NetworkStringTable<'s> {
raw: NonNull<sys::INetworkStringTable>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> NetworkStringTable<'s> {
const unsafe fn from_raw(raw: NonNull<sys::INetworkStringTable>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
#[doc(alias("AddString"))]
#[expect(
clippy::should_implement_trait,
reason = "adds to the table, unlike `Add::add`"
)]
pub fn add(self, string: &CStr) -> Result<usize, AddStringError> {
let index = unsafe {
vcall!(self.as_ptr() => INetworkStringTable_AddString(true, string.as_ptr(), UNKNOWN_STRING_LENGTH, ptr::null()))
};
usize::try_from(index)
.ok()
.filter(|_| index != c_int::from(INVALID_STRING_INDEX))
.ok_or(AddStringError)
}
#[doc(alias("AddString", "SetStringUserData"))]
pub fn add_with_user_data(self, string: &CStr, data: &[u8]) -> Result<usize, AddStringError> {
if data.is_empty() || data.len() > MAX_USER_DATA_LEN {
return Err(AddStringError);
}
let length = c_int::try_from(data.len()).map_err(|_| AddStringError)?;
let index = unsafe {
vcall!(self.as_ptr() => INetworkStringTable_AddString(true, string.as_ptr(), length, data.as_ptr().cast()))
};
usize::try_from(index)
.ok()
.filter(|_| index != c_int::from(INVALID_STRING_INDEX))
.ok_or(AddStringError)
}
pub const fn as_ptr(self) -> *mut sys::INetworkStringTable {
self.raw.as_ptr()
}
#[doc(alias("FindStringIndex"))]
pub fn find(self, string: &CStr) -> Option<usize> {
let index = unsafe {
vcall!(self.as_ptr() => INetworkStringTable_FindStringIndex(string.as_ptr()))
};
usize::try_from(index)
.ok()
.filter(|_| index != c_int::from(INVALID_STRING_INDEX))
}
pub fn is_empty(self) -> bool {
self.len() == 0
}
#[doc(alias("GetNumStrings"))]
pub fn len(self) -> usize {
usize::try_from(unsafe { vcall!(self.as_ptr() => INetworkStringTable_GetNumStrings()) })
.unwrap_or(0)
}
#[doc(alias("GetMaxStrings"))]
pub fn max_len(self) -> usize {
usize::try_from(unsafe { vcall!(self.as_ptr() => INetworkStringTable_GetMaxStrings()) })
.unwrap_or(0)
}
#[doc(alias("GetTableName"))]
pub fn name(self) -> &'s CStr {
unsafe { borrow_cstr(vcall!(self.as_ptr() => INetworkStringTable_GetTableName())) }
.unwrap_or_default()
}
#[doc(alias("GetString"))]
pub fn string(self, index: usize) -> Option<CString> {
if index >= self.len() {
return None;
}
let index = c_int::try_from(index).ok()?;
unsafe { copy_cstr(vcall!(self.as_ptr() => INetworkStringTable_GetString(index))) }
}
#[doc(alias("GetStringUserData"))]
pub fn user_data(self, index: usize) -> Option<Vec<u8>> {
if index >= self.len() {
return None;
}
let index = c_int::try_from(index).ok()?;
let mut len: c_int = 0;
let data = unsafe {
vcall!(self.as_ptr() => INetworkStringTable_GetStringUserData(index, &raw mut len))
};
let len = usize::try_from(len).unwrap_or(0);
if data.is_null() || len == 0 {
return Some(Vec::new());
}
Some(unsafe { std::slice::from_raw_parts(data.cast::<u8>(), len) }.to_vec())
}
}
impl<'s> NetworkStringTables<'s> {
fn count(self) -> c_int {
unsafe { vcall!(self.as_ptr() => INetworkStringTableContainer_GetNumTables()) }
}
#[doc(alias("FindTable"))]
pub fn find(self, name: &CStr) -> Option<NetworkStringTable<'s>> {
let table = unsafe {
vcall!(self.as_ptr() => INetworkStringTableContainer_FindTable(name.as_ptr()))
};
NonNull::new(table).map(|table| unsafe { NetworkStringTable::from_raw(table) })
}
#[doc(alias("GetTable"))]
pub fn get(self, id: usize) -> Option<NetworkStringTable<'s>> {
let id = c_int::try_from(id).ok().filter(|&id| id < self.count())?;
let table = unsafe { vcall!(self.as_ptr() => INetworkStringTableContainer_GetTable(id)) };
NonNull::new(table).map(|table| unsafe { NetworkStringTable::from_raw(table) })
}
pub fn is_empty(self) -> bool {
self.len() == 0
}
#[doc(alias("IsSoundPrecached"))]
pub fn is_sound_precached(self, sample: &CStr) -> bool {
self.find(SOUND_PRECACHE)
.is_some_and(|table| table.find(sample).is_some())
}
#[doc(alias("GetNumTables"))]
pub fn len(self) -> usize {
usize::try_from(self.count()).unwrap_or(0)
}
#[doc(alias("GetParticleSystemIndex"))]
pub fn particle_system_index(self, name: &CStr) -> Option<usize> {
self.find(PARTICLE_EFFECT_NAMES)?.find(name)
}
}
pub fn add_downloadable(server: Server<'_>, path: &CStr) -> Result<usize, AddDownloadableError> {
let engine = server.valve_engine()?;
let table = server
.network_string_tables()?
.find(DOWNLOADABLES)
.ok_or(AddDownloadableError::MissingTable)?;
engine
.with_unlocked_string_tables(|| table.add(path))
.map_err(|AddStringError| AddDownloadableError::Refused)
}