use super::*;
use crate::interfaces::{NetworkStringTables, ValveEngine};
use crate::server::{Game, Module};
use crate::test_support::leak;
use crate::test_support::server::{export, mock_server, null_server};
use sdk_raw::test_support::{mock_vtable, unexpected_call};
use std::cell::{Cell, RefCell};
use std::ffi::{CString, c_char, c_int};
use std::ptr::null_mut;
#[repr(C)]
struct TableObject {
interface: sys::INetworkStringTable,
name: &'static CStr,
strings: RefCell<Vec<CString>>,
max: c_int,
}
thread_local! {
static PRECACHED: RefCell<Vec<(&'static CStr, CString, bool)>> = const { RefCell::new(Vec::new()) };
static REFUSES: Cell<bool> = const { Cell::new(false) };
static TABLES: RefCell<Vec<*mut TableObject>> = const { RefCell::new(Vec::new()) };
}
fn add(table: &'static CStr, name: *const c_char, preload: bool) -> c_int {
let name = unsafe { CStr::from_ptr(name) }.to_owned();
PRECACHED.with_borrow_mut(|precached| precached.push((table, name.clone(), preload)));
if REFUSES.get() {
return -1;
}
let table = find(table).expect("the wrapper found the table first");
let mut strings = unsafe { (*table).strings.borrow_mut() };
let index = strings
.iter()
.position(|string| *string == name)
.unwrap_or_else(|| {
strings.push(name);
strings.len() - 1
});
c_int::try_from(index).unwrap()
}
fn find(name: &CStr) -> Option<*mut TableObject> {
TABLES.with_borrow(|tables| {
tables
.iter()
.copied()
.find(|&table| unsafe { (*table).name } == name)
})
}
unsafe extern "C" fn find_table(
_: *const sys::INetworkStringTableContainer,
name: *const c_char,
) -> *mut sys::INetworkStringTable {
find(unsafe { CStr::from_ptr(name) }).map_or(null_mut(), |table| table.cast())
}
fn mock_engine(tables: &[(&'static CStr, c_int)], initial: &[&CStr]) {
let (engine_vtable, container_vtable, table_vtable) = unsafe {
(
mock_vtable::<sys::IVEngineServer__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IVEngineServer_PrecacheDecal).write(precache_decal);
(&raw mut (*vtable).IVEngineServer_PrecacheGeneric).write(precache_generic);
(&raw mut (*vtable).IVEngineServer_PrecacheModel).write(precache_model);
},
),
mock_vtable::<sys::INetworkStringTableContainer__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).INetworkStringTableContainer_FindTable).write(find_table);
},
),
mock_vtable::<sys::INetworkStringTable__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).INetworkStringTable_FindStringIndex).write(table_find);
(&raw mut (*vtable).INetworkStringTable_GetMaxStrings).write(table_max);
(&raw mut (*vtable).INetworkStringTable_GetNumStrings).write(table_len);
},
),
)
};
let table_vtable = Box::leak(table_vtable);
let tables = tables
.iter()
.map(|&(name, max)| {
leak(TableObject {
interface: sys::INetworkStringTable {
vtable_: &raw const *table_vtable,
},
name,
strings: RefCell::new(initial.iter().map(|&string| string.to_owned()).collect()),
max,
})
})
.collect();
TABLES.set(tables);
PRECACHED.take();
REFUSES.set(false);
export(
Module::Engine,
ValveEngine::VERSION,
leak(sys::IVEngineServer {
vtable_: Box::leak(engine_vtable),
}),
);
export(
Module::Engine,
NetworkStringTables::VERSION,
leak(sys::INetworkStringTableContainer {
vtable_: Box::leak(container_vtable),
}),
);
}
#[test]
fn names_are_precached_into_their_tables() {
mock_engine(
&[
(DECAL_PRECACHE, 8),
(GENERIC_PRECACHE, 8),
(MODEL_PRECACHE, 8),
],
&[c"world"],
);
let scope = ();
let server = mock_server(&scope);
assert_eq!(server.precache_model(c"models/box.mdl", true), Ok(1));
assert_eq!(server.precache_model(c"models/box.mdl", false), Ok(1));
assert_eq!(server.precache_model(c"world", false), Ok(0));
assert_eq!(server.precache_decal(c"decals/scorch1", false), Ok(1));
assert_eq!(server.precache_generic(c"particles/fire.pcf", true), Ok(1));
assert_eq!(
PRECACHED.take(),
[
(MODEL_PRECACHE, c"models/box.mdl".to_owned(), true),
(MODEL_PRECACHE, c"models/box.mdl".to_owned(), false),
(MODEL_PRECACHE, c"world".to_owned(), false),
(DECAL_PRECACHE, c"decals/scorch1".to_owned(), false),
(GENERIC_PRECACHE, c"particles/fire.pcf".to_owned(), true),
]
);
REFUSES.set(true);
assert_eq!(
server.precache_model(c"models/box.mdl", false),
Err(PrecacheError::Refused)
);
}
#[test]
fn names_the_engine_stops_the_server_for_are_refused() {
mock_engine(&[(MODEL_PRECACHE, 2)], &[c"world", c"models/box.mdl"]);
let scope = ();
let server = mock_server(&scope);
for name in [
c"",
c" models/box.mdl",
c"\tmodels/box.mdl",
c"\xC3\xA9.mdl",
] {
assert_eq!(
server.precache_model(name, false),
Err(PrecacheError::InvalidName),
"{name:?}"
);
}
assert_eq!(
server.precache_model(c"models/crate.mdl", false),
Err(PrecacheError::TableFull)
);
assert_eq!(server.precache_model(c"models/box.mdl", false), Ok(1));
assert_eq!(
server.precache_decal(c"decals/scorch1", false),
Err(PrecacheError::NoTable)
);
assert_eq!(PRECACHED.take().len(), 1);
let scope = ();
assert!(matches!(
null_server(Game::TeamFortress2, &scope).precache_generic(c"particles/fire.pcf", false),
Err(PrecacheError::Interface(_))
));
}
unsafe extern "C" fn precache_decal(
_: *mut sys::IVEngineServer,
name: *const c_char,
preload: bool,
) -> c_int {
add(DECAL_PRECACHE, name, preload)
}
unsafe extern "C" fn precache_generic(
_: *mut sys::IVEngineServer,
name: *const c_char,
preload: bool,
) -> c_int {
add(GENERIC_PRECACHE, name, preload)
}
unsafe extern "C" fn precache_model(
_: *mut sys::IVEngineServer,
name: *const c_char,
preload: bool,
) -> c_int {
add(MODEL_PRECACHE, name, preload)
}
unsafe extern "C" fn table_find(this: *mut sys::INetworkStringTable, name: *const c_char) -> c_int {
let (table, name) = unsafe { (&*this.cast::<TableObject>(), CStr::from_ptr(name)) };
table
.strings
.borrow()
.iter()
.position(|string| string.as_c_str() == name)
.map_or(
c_int::from(sdk_raw::interfaces::network_string_tables::INVALID_STRING_INDEX),
|index| c_int::try_from(index).unwrap(),
)
}
unsafe extern "C" fn table_len(this: *const sys::INetworkStringTable) -> c_int {
let table = unsafe { &*this.cast::<TableObject>() };
c_int::try_from(table.strings.borrow().len()).unwrap()
}
unsafe extern "C" fn table_max(this: *const sys::INetworkStringTable) -> c_int {
unsafe { (*this.cast::<TableObject>()).max }
}