- Препроцессор GMod: continue внутри for/while/repeat -> goto + уникальная метка; учтены строки/комментарии, вложенные циклы и функции, завершающие break/return (оборачиваются в do ... end). widget_bones.lua грузится: энтити 6 из 6. - include(): поиск рядом с текущим файлом и в lua/ игры, тот же препроцессор, лимиты размера/глубины, защита от циклов и выхода за каталог игры. - LuaRuntime::load_gamemode(): init.lua и cl_init.lua, сбор ошибок без падения. Sandbox init.lua выполняется, cl_init.lua упирается в VGUI (AccessorFunc); реестры list/scripted_ents после загрузки целы. - Безопасные заглушки загрузочного слоя Sandbox: CLIENT/SERVER, is*, FindMetaTable, cleanup, drive, duplicator, net, cvars, player_manager. - Тесты: юнит-тесты препроцессора, include на временных файлах и реальных lua/includes, загрузка sandbox на ассетах.
155 lines
6.4 KiB
Rust
155 lines
6.4 KiB
Rust
//! Загрузка настоящих скриптов GMod (оружие и энтити) из установки игры.
|
||
//!
|
||
//! Без ассетов тест пропускается: путь задаётся `SOURCE_RS_ASSETS` или
|
||
//! `~/gmod_server/server/garrysmod`.
|
||
|
||
use glua::LuaRuntime;
|
||
use std::path::PathBuf;
|
||
|
||
fn game_dir() -> Option<PathBuf> {
|
||
let root = std::env::var_os("SOURCE_RS_ASSETS")
|
||
.map(PathBuf::from)
|
||
.or_else(|| {
|
||
std::env::var_os("HOME").map(|home| PathBuf::from(home).join("gmod_server/server"))
|
||
})?;
|
||
let game = root.join("garrysmod");
|
||
game.is_dir().then_some(game)
|
||
}
|
||
|
||
#[test]
|
||
fn real_gmod_weapons_and_entities_load() {
|
||
let Some(game) = game_dir() else {
|
||
eprintln!("пропуск: ассеты Garry's Mod не найдены");
|
||
return;
|
||
};
|
||
let runtime = LuaRuntime::new().expect("рантайм");
|
||
let (weapons, entities, failed) = runtime.load_scripts(&game);
|
||
eprintln!("скрипты: оружия {weapons}, энтити {entities}, ошибок {failed}");
|
||
assert!(weapons >= 3, "мало оружия: {weapons}");
|
||
// `widget_bones.lua` использует GMod-расширение `continue`; после
|
||
// препроцессора грузятся все 6 энтити.
|
||
assert!(entities >= 6, "мало энтити: {entities}");
|
||
assert_eq!(failed, 0, "ошибки загрузки скриптов");
|
||
|
||
// Поля SWEP прочитаны (Primary создаётся до выполнения скрипта).
|
||
let clip = runtime
|
||
.probe("return tostring(weapons.Get('weapon_fists').Primary.ClipSize)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(clip, "-1", "Primary.ClipSize кулаков");
|
||
let model = runtime
|
||
.probe("return weapons.Get('weapon_fists').ViewModel.path")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert!(model.contains("c_arms"), "ViewModel: {model}");
|
||
|
||
// Реестр list.Get("Weapon") — то, из чего строится меню.
|
||
let count = runtime
|
||
.probe("local n=0 for _ in pairs(list.Get('Weapon') or {}) do n=n+1 end return tostring(n)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
eprintln!("list.Get('Weapon') = {count}");
|
||
assert!(
|
||
count.parse::<usize>().unwrap_or(0) >= 3,
|
||
"list.Get('Weapon'): {count}"
|
||
);
|
||
|
||
// Энтити: запись доступна и помечена как спавнабельная.
|
||
let entity = runtime
|
||
.probe("return tostring(scripted_ents.Get('sent_ball') ~= nil)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(entity, "true", "scripted_ents.Get('sent_ball')");
|
||
}
|
||
|
||
#[test]
|
||
fn real_gmod_include_loads_lua_includes() {
|
||
let Some(game) = game_dir() else {
|
||
eprintln!("пропуск: ассеты Garry's Mod не найдены");
|
||
return;
|
||
};
|
||
let runtime = LuaRuntime::new().expect("рантайм");
|
||
runtime.set_file_root(&game).expect("корень игры");
|
||
// Реальный файл из lua/includes: расширение метатаблицы Vector.
|
||
runtime
|
||
.run(
|
||
"includes/extensions/vector.lua",
|
||
br#"
|
||
include( "includes/extensions/vector.lua" )
|
||
include( "includes/extensions/angle.lua" )
|
||
"#,
|
||
)
|
||
.expect("lua/includes загрузились");
|
||
let to_color = runtime
|
||
.probe("return type(FindMetaTable('Vector').ToColor)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(to_color, "function", "Vector:ToColor из includes");
|
||
let snap = runtime
|
||
.probe("return type(FindMetaTable('Angle').SnapTo)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(snap, "function", "Angle:SnapTo из includes");
|
||
}
|
||
|
||
#[test]
|
||
fn real_gmod_sandbox_gamemode_loads_without_breaking_registries() {
|
||
let Some(game) = game_dir() else {
|
||
eprintln!("пропуск: ассеты Garry's Mod не найдены");
|
||
return;
|
||
};
|
||
let runtime = LuaRuntime::new().expect("рантайм");
|
||
runtime.set_file_root(&game).expect("корень игры");
|
||
let (_, entities, failed) = runtime.load_scripts(&game);
|
||
assert_eq!(
|
||
failed, 0,
|
||
"скрипты оружия/энтити должны грузиться без ошибок"
|
||
);
|
||
|
||
let before = runtime
|
||
.probe("local n=0 for _ in pairs(list.Get('SpawnableEntities') or {}) do n=n+1 end return tostring(n)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert!(
|
||
before.parse::<usize>().unwrap_or(0) >= entities,
|
||
"SpawnableEntities до геймода: {before}"
|
||
);
|
||
|
||
let report = runtime.load_gamemode(&game, "sandbox");
|
||
eprintln!("геймод sandbox: загружено {:?}", report.loaded);
|
||
for error in &report.errors {
|
||
eprintln!("геймод sandbox: ошибка: {error}");
|
||
}
|
||
assert!(
|
||
report.loaded.iter().any(|file| file == "init.lua"),
|
||
"init.lua Sandbox должен выполниться"
|
||
);
|
||
// cl_init.lua тянет VGUI/Derma: ошибка ожидаема, но загрузчик обязан
|
||
// попытаться выполнить файл и собрать ошибку, а не упасть.
|
||
assert!(
|
||
report
|
||
.errors
|
||
.iter()
|
||
.any(|error| error.starts_with("cl_init.lua:")),
|
||
"ошибка cl_init.lua должна быть собрана: {:?}",
|
||
report.errors
|
||
);
|
||
|
||
// Реестры не должны пострадать от загрузки геймода: именно из них
|
||
// строятся меню спавна и выдача оружия.
|
||
let after = runtime
|
||
.probe("local n=0 for _ in pairs(list.Get('SpawnableEntities') or {}) do n=n+1 end return tostring(n)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(after, before, "SpawnableEntities после геймода");
|
||
let ball = runtime
|
||
.probe("return tostring(scripted_ents.Get('sent_ball') ~= nil)")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(ball, "true", "sent_ball после геймода");
|
||
let gm_name = runtime
|
||
.probe("return GAMEMODE and GAMEMODE.Name or ''")
|
||
.expect("probe")
|
||
.unwrap_or_default();
|
||
assert_eq!(gm_name, "Sandbox", "GAMEMODE.Name из shared.lua");
|
||
}
|