gmod-rs/crates/lua/tests/scripts.rs
Niko Marmeladkov 0b82cd77f3 Lua: continue, include и загрузка геймода
- Препроцессор 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 на ассетах.
2026-10-24 13:29:30 +03:00

155 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Загрузка настоящих скриптов 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");
}