Lua: поля ENT-скриптов и модель-представитель для спавн-меню
This commit is contained in:
parent
70ee8d9ece
commit
f41bfde690
1 changed files with 132 additions and 0 deletions
|
|
@ -113,6 +113,40 @@ impl LuaRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
/// Строковое поле ENT-скрипта (`Model`, `WorldModel`, `PrintName`, ...).
|
||||
///
|
||||
/// Нужен спавн-меню: список `SpawnableEntities` хранит только имена, а
|
||||
/// модель сущности лежит в её таблице. Как и у оружия, `Model("...")`
|
||||
/// приходит таблицей с полем `path`.
|
||||
pub fn entity_string(&self, class: &str, field: &str) -> Option<String> {
|
||||
let globals = self.lua.globals();
|
||||
let store: Table = globals.get("__sr_ents").ok()?;
|
||||
let Value::Table(ent) = store.get::<Value>(class).ok()? else {
|
||||
return None;
|
||||
};
|
||||
match ent.get::<Value>(field) {
|
||||
Ok(Value::String(value)) => Some(value.to_string_lossy().to_string()),
|
||||
Ok(Value::Table(value)) => value.get::<Option<String>>("path").ok().flatten(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Булево поле ENT-скрипта (в спавн-меню — `Spawnable`).
|
||||
///
|
||||
/// `nil` и не-булевы значения дают `None`: фильтр меню отличает
|
||||
/// «явно false» от «поле не задано».
|
||||
pub fn entity_bool(&self, class: &str, field: &str) -> Option<bool> {
|
||||
let globals = self.lua.globals();
|
||||
let store: Table = globals.get("__sr_ents").ok()?;
|
||||
let Value::Table(ent) = store.get::<Value>(class).ok()? else {
|
||||
return None;
|
||||
};
|
||||
match ent.get::<Value>(field) {
|
||||
Ok(Value::Boolean(value)) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Загружает скрипты GMod: `lua/weapons/*.lua` и `lua/entities/*.lua`.
|
||||
///
|
||||
/// Каждый скрипт выполняется со свежей таблицей `SWEP`/`ENT` (у оружия
|
||||
|
|
@ -166,6 +200,16 @@ impl LuaRuntime {
|
|||
failed += 1;
|
||||
continue;
|
||||
}
|
||||
// Модель спавн-меню: у ENT-скриптов её часто задают только в
|
||||
// коде (`self:SetModel("models/...")`), а не полем `ENT.Model`.
|
||||
// Запоминаем первый литерал `models/*.mdl` — он и будет
|
||||
// представителем сущности в списке спавна.
|
||||
if kind == "entity"
|
||||
&& let Some(model) = find_model_literal(&source)
|
||||
&& let Err(err) = table.set("__sr_preview_model", model)
|
||||
{
|
||||
eprintln!("скрипт {}: модель для спавна: {err}", file.display());
|
||||
}
|
||||
// GMod-синтаксис (!, &&, ||) приводится препроцессором —
|
||||
// без него скрипты падают на первой же строке с `!`.
|
||||
if let Err(err) = self.run(name, source.as_bytes()) {
|
||||
|
|
@ -742,6 +786,47 @@ pub fn preprocess(source: &str) -> String {
|
|||
out
|
||||
}
|
||||
|
||||
/// Первый литерал `models/*.mdl` в исходнике ENT-скрипта.
|
||||
///
|
||||
/// Спавн-меню нужно чем-то показать сущность, у которой модель задаётся
|
||||
/// вызовом `SetModel` в `Initialize`, а не полем `ENT.Model`. Поиск
|
||||
/// ограничен: путь короче 256 байт, только до кавычки/пробела/закрывающей
|
||||
/// скобки — данные из файла не должны влиять на границы среза.
|
||||
fn find_model_literal(source: &str) -> Option<String> {
|
||||
const PREFIX: &[u8] = b"models/";
|
||||
const MAX_PATH: usize = 256;
|
||||
let bytes = source.as_bytes();
|
||||
let mut i = 0usize;
|
||||
while i < bytes.len() {
|
||||
if i + PREFIX.len() <= bytes.len()
|
||||
&& bytes[i..i + PREFIX.len()].eq_ignore_ascii_case(PREFIX)
|
||||
{
|
||||
let mut end = i + PREFIX.len();
|
||||
while end < bytes.len() && end - i < MAX_PATH {
|
||||
let b = bytes[end];
|
||||
if b.is_ascii_whitespace() || matches!(b, b'"' | b'\'' | b')' | b',' | b';' | 0) {
|
||||
break;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
let Some(candidate) = source.get(i..end) else {
|
||||
// Лимит длины мог разрезать многобайтовый символ: пропускаем.
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
if candidate.len() > PREFIX.len()
|
||||
&& candidate
|
||||
.get(candidate.len().saturating_sub(4)..)
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case(".mdl"))
|
||||
{
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Если на позиции длинная скобка `[=*[`, возвращает её длину.
|
||||
fn long_bracket(bytes: &[u8], at: usize) -> (usize, ()) {
|
||||
if bytes.get(at) != Some(&b'[') {
|
||||
|
|
@ -889,4 +974,51 @@ local t = [[raw ! text]]
|
|||
assert_eq!(out[4], "5 0 0", "поле записывается");
|
||||
assert_eq!(out[5], "10", "math.Clamp");
|
||||
}
|
||||
|
||||
/// Реестр ENT-скриптов отдаёт поля, по которым строится спавн-меню:
|
||||
/// класс, `Spawnable` и модель.
|
||||
#[test]
|
||||
fn entity_registry_exposes_spawn_fields() {
|
||||
let rt = LuaRuntime::new().expect("рантайм");
|
||||
rt.run(
|
||||
"sent_test.lua",
|
||||
br#"
|
||||
local ENT = {
|
||||
Type = "sent_test",
|
||||
Spawnable = true,
|
||||
Model = Model("models/props_junk/watermelon01.mdl"),
|
||||
}
|
||||
scripted_ents.Register(ENT, "sent_test")
|
||||
"#,
|
||||
)
|
||||
.expect("скрипт");
|
||||
assert_eq!(
|
||||
rt.entity_string("sent_test", "Model").as_deref(),
|
||||
Some("models/props_junk/watermelon01.mdl")
|
||||
);
|
||||
assert_eq!(rt.entity_bool("sent_test", "Spawnable"), Some(true));
|
||||
assert_eq!(rt.entity_bool("sent_test", "AdminOnly"), None);
|
||||
assert_eq!(rt.entity_string("нет_такого", "Model"), None);
|
||||
}
|
||||
|
||||
/// Модель `SetModel` в коде ENT-скрипта находится без разбора Lua:
|
||||
/// только литерал, с границами и лимитом длины.
|
||||
#[test]
|
||||
fn model_literal_from_entity_source() {
|
||||
let src = r#"function ENT:Initialize() self:SetModel( "models/Combine_Helicopter/helicopter_bomb01.mdl" ) end"#;
|
||||
assert_eq!(
|
||||
find_model_literal(src).as_deref(),
|
||||
Some("models/Combine_Helicopter/helicopter_bomb01.mdl")
|
||||
);
|
||||
assert_eq!(find_model_literal("no models here"), None);
|
||||
// Обрывок без расширения не считается моделью.
|
||||
assert_eq!(
|
||||
find_model_literal("SetModel('models/props_junk/a b.mdl')"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
find_model_literal("prefab = 'models/props_junk/popcan01a.mdl'"),
|
||||
Some("models/props_junk/popcan01a.mdl".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue