//! Загрузка настоящих скриптов GMod (оружие и энтити) из установки игры. //! //! Без ассетов тесты пропускаются: путь задаётся `SOURCE_RS_ASSETS` или //! `~/gmod_server/server/garrysmod`. use glua::LuaRuntime; use std::path::PathBuf; fn game_dir() -> Option { 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::().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::().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"); } /// Реальные SWEP-скрипты вызываются клиентом: Deploy, атака, Think, Holster. /// /// Тест важен тем, что не мокает GMod-API, а прогоняет **настоящие** файлы /// оружия из установки: любая отсутствующая функция (`CurTime`, `IsValid`, /// `util.TraceLine`, `GetViewModel`, ...) здесь станет ошибкой Lua. #[test] fn real_gmod_weapon_methods_run_without_errors() { let Some(game) = game_dir() else { eprintln!("пропуск: ассеты Garry's Mod не найдены"); return; }; let runtime = LuaRuntime::new().expect("рантайм"); let (weapons, _, _) = runtime.load_scripts(&game); assert!(weapons >= 3, "мало оружия: {weapons}"); for class in ["weapon_fists", "weapon_flechettegun", "weapon_medkit"] { for method in [ "SetupDataTables", "Initialize", "Deploy", "PrimaryAttack", "Think", "SecondaryAttack", "Holster", ] { runtime .call_weapon_method(class, method) .unwrap_or_else(|err| panic!("{class}:{method}: {err}")); } } // Магазин медика: после атаки по пустой трассе он не тратится, но // значение обязано читаться как число (общий контракт Clip1). let clip = runtime .call_weapon_number("weapon_medkit", "Clip1") .expect("Clip1 медика"); assert!(clip >= 0.0, "Clip1: {clip}"); let clip_size = runtime .weapon_number("weapon_medkit", "Primary.ClipSize") .expect("Primary.ClipSize"); assert_eq!(clip_size, 100.0); }