gmod-rs/crates/lua/tests/scripts.rs
Niko Marmeladkov 210182a940
Some checks are pending
CI / checks (push) Waiting to run
Слияние feat/swep: реестр оружия в клиенте, слоты, атака, HUD
Из ветки feat/swep (a46e4d8, 1fe36b6, 5a8f971):
- выбор SWEP из list.Get('Weapon') (фильтр Spawnable + существующая
  ViewModel), слоты 1..9, SOURCE_RS_SWEP для старта, Holster/Deploy при смене,
  вьюмодель из SWEP.ViewModel (SOURCE_RS_VIEW_MODEL — override);
- ЛКМ → PrimaryAttack через call_weapon_method, автоматический/одиночный огонь,
  Think каждый кадр; без SWEP — прежние физган и fire-анимация;
- HUD: PrintName и патроны Clip1/ClipSize;
- заглушки GMod-API под реальные скрипты: CurTime, IsValid, NULL, player.GetAll,
  util.TraceLine/TraceHull/TraceEntity/TracePhysics, trace.*, sound.*/EmitSound,
  константы PLAYER_ATTACK*/ACT_VM_*/MASK_*, база SWEP (GetOwner/GetViewModel/
  NetworkVar/SetNextPrimaryFire/SetHoldType/Clip1/EmitSound/SendWeaponAnim/
  SequenceDuration/ShootEffects/Remove), game.SinglePlayer как функция,
  GetConVar для sv_defaultdeployspeed/phys_pushscale;
- тест real_gmod_weapon_methods_run_without_errors прогоняет настоящие
  weapon_fists/flechettegun/medkit (SetupDataTables..Holster) без ошибок Lua.

Конфликт в crates/lua/tests/scripts.rs разрешён: сохранены тесты include и
геймода из feat/lua-pre и добавлен тест SWEP из feat/swep.
Проверено: 27 сьютов, 46+4+4 теста glua зелёные.
2026-10-24 13:33:50 +03:00

198 lines
8.2 KiB
Rust
Raw Permalink 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");
}
/// Реальные 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);
}