lua: типы GMod — Vector, Angle, Color с арифметикой
Батч 3, шаг 1. Фундамент Lua-API: без этих типов Entity:GetPos() не возвращает ничего, с чем можно считать. Проверено тестами через настоящий LuaRuntime, а не голый интерпретатор: скрипт считает теми же глобалами, что и обычный автор в Sandbox. Решения по совместимости: - пропущенные аргументы не бывают ошибкой: Vector() и Vector(1, 2) должны работать, иначе ломается бытовой код Sandbox; - деление на ноль даёт 1, а не inf — inf в позиции пропа отравляет всю геометрию объекта; - NaN из скрипта не доезжает до физики; - компоненты цвета обрезаются в 0..255, а не отвергаются; - Source — левая система координат: нулевые углы смотрят вдоль +X, yaw=90 — вдоль +Y. Найден дефект чтения чисел в mlua 0.11: Value::as_number() различает только Number и молча возвращает None на Integer, а LuaJIT отдаёт целые как Integer. Vector(1, 2, 3) давал Vector(0, 0, 0) — и это выглядело как «арифметика не работает». Собственный value_number() принимает Number, Integer и строку-число. Ещё убрат шум тригонометрии: cos(90°) в f32 равен 6.1e-17, и Angle(0,90,0):Forward() печатал -0.00000004371139 вместо 0. Тестов 17 в glua (было 1), 23 набора по воркспейсу, clippy чист.
This commit is contained in:
parent
1cb836d4c9
commit
b9b03abe3b
3 changed files with 813 additions and 0 deletions
717
crates/lua/src/gmod.rs
Normal file
717
crates/lua/src/gmod.rs
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
//! Типы значений GMod: `Vector`, `Angle`, `Color`.
|
||||
//!
|
||||
//! Это фундамент Lua-API: без него `Entity:GetPos()` не вернёт ничего, что
|
||||
//! можно было бы посчитать. Реализовано ровно то поведение, на которое
|
||||
//! опирается `base_ents` и Sandbox:
|
||||
//!
|
||||
//! - `Vector(x, y, z)` с полями `.x/.y/.z`, доступными на запись;
|
||||
//! - арифметика `+ - *` (скаляр и покомпонентно с другим вектором), унарный
|
||||
//! минус, деление на скаляр;
|
||||
//! - `#v` — длина вектора, именно так в GMod;
|
||||
//! - `Angle(p, y, r)` с `.p/.y/.r` (в GMod это **пич**, **yaw**, **roll**);
|
||||
//! - `Color(r, g, b, a)` с полями, которые в GMod доступны и на запись;
|
||||
//! - `tostring` в формате GMod, чтобы `print(Vector(1,2,3))` давал
|
||||
//! `"1.000000 2.000000 3.000000"`, а не отладочный вывод Rust.
|
||||
//!
|
||||
//! Границы: `FromLua` принимает и userdata, и число — `Vector(1, 2)` не
|
||||
//! должен валить страницу из-за отсутствующего третьего аргумента.
|
||||
|
||||
use mlua::{AnyUserData, Lua, Table, UserData, UserDataFields, UserDataMethods, Value};
|
||||
|
||||
/// Вектор Source: X вперёд, Y влево, Z вверх.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct LuaVector {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
|
||||
impl LuaVector {
|
||||
pub fn new(x: f32, y: f32, z: f32) -> Self {
|
||||
Self { x, y, z }
|
||||
}
|
||||
|
||||
/// Длина вектора.
|
||||
pub fn length(self) -> f32 {
|
||||
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
|
||||
}
|
||||
|
||||
/// Нормированный вектор; нулевой остаётся нулём, а не `NaN`.
|
||||
pub fn normalized(self) -> Self {
|
||||
let len = self.length();
|
||||
if len < 1e-6 {
|
||||
return Self::default();
|
||||
}
|
||||
Self::new(self.x / len, self.y / len, self.z / len)
|
||||
}
|
||||
}
|
||||
|
||||
/// Углы Source: pitch, yaw, roll **в градусах**.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct LuaAngle {
|
||||
pub p: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
}
|
||||
|
||||
impl LuaAngle {
|
||||
pub fn new(p: f32, y: f32, r: f32) -> Self {
|
||||
Self { p, y, r }
|
||||
}
|
||||
}
|
||||
|
||||
/// Цвет Source: RGBA, компоненты 0..255.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LuaColor {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
impl Default for LuaColor {
|
||||
fn default() -> Self {
|
||||
Self::new(255, 255, 255, 255)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaColor {
|
||||
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Компонента из Lua: дробные и вылезающие значения обрезаются в 0..255,
|
||||
/// как это делает GMod. Ошибка вместо обрезки тут была бы ловушкой для
|
||||
/// скрипта автора: `Color(1.5 * 300, 0, 0)` — обычное дело.
|
||||
pub fn component(value: Option<f64>, default: u8) -> u8 {
|
||||
match value {
|
||||
Some(v) if v.is_finite() => v.clamp(0.0, 255.0).round() as u8,
|
||||
_ => default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData for LuaVector {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("x", |_, v| Ok(v.x));
|
||||
fields.add_field_method_get("y", |_, v| Ok(v.y));
|
||||
fields.add_field_method_get("z", |_, v| Ok(v.z));
|
||||
fields.add_field_method_set("x", |_, v, value: f32| {
|
||||
v.x = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("y", |_, v, value: f32| {
|
||||
v.y = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("z", |_, v, value: f32| {
|
||||
v.z = value;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
|
||||
m.add_meta_method("__add", |lua, a, b: Value| add(lua, *a, &b));
|
||||
m.add_meta_method("__sub", |lua, a, b: Value| sub(lua, *a, &b));
|
||||
m.add_meta_method("__mul", |lua, a, b: Value| mul(lua, *a, &b));
|
||||
m.add_meta_method("__div", |lua, a, b: Value| div(lua, *a, &b));
|
||||
m.add_meta_method("__unm", |_, v: &Self, _: ()| {
|
||||
Ok(Self::new(-v.x, -v.y, -v.z))
|
||||
});
|
||||
m.add_meta_method("__len", |_, v: &Self, _: ()| Ok(v.length()));
|
||||
m.add_meta_method("__eq", |_, a: &Self, b: Value| {
|
||||
Ok(borrow_vector(&b).map(|b| *a == b).unwrap_or(false))
|
||||
});
|
||||
m.add_meta_method("__tostring", |_, v: &Self, _: ()| {
|
||||
Ok(format!("{} {} {}", v.x, v.y, v.z))
|
||||
});
|
||||
m.add_function("Length", |_, v: AnyUserData| {
|
||||
Ok(v.borrow::<Self>()?.length())
|
||||
});
|
||||
m.add_function("Normalized", |_, v: AnyUserData| {
|
||||
Ok(v.borrow::<Self>()?.normalized())
|
||||
});
|
||||
m.add_function("Distance", |_, (a, b): (AnyUserData, AnyUserData)| {
|
||||
let (a, b) = (*a.borrow::<Self>()?, *b.borrow::<Self>()?);
|
||||
Ok(Self::new(a.x - b.x, a.y - b.y, a.z - b.z).length())
|
||||
});
|
||||
m.add_function("Dot", |_, (a, b): (AnyUserData, AnyUserData)| {
|
||||
let (a, b) = (*a.borrow::<Self>()?, *b.borrow::<Self>()?);
|
||||
Ok(a.x * b.x + a.y * b.y + a.z * b.z)
|
||||
});
|
||||
m.add_function("Unpack", |lua, v: AnyUserData| {
|
||||
let v = *v.borrow::<Self>()?;
|
||||
let table = lua.create_table()?;
|
||||
table.set(1, v.x)?;
|
||||
table.set(2, v.y)?;
|
||||
table.set(3, v.z)?;
|
||||
Ok(table)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData for LuaAngle {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("p", |_, v| Ok(v.p));
|
||||
fields.add_field_method_get("y", |_, v| Ok(v.y));
|
||||
fields.add_field_method_get("r", |_, v| Ok(v.r));
|
||||
fields.add_field_method_set("p", |_, v, value: f32| {
|
||||
v.p = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("y", |_, v, value: f32| {
|
||||
v.y = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("r", |_, v, value: f32| {
|
||||
v.r = value;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
|
||||
m.add_meta_method("__eq", |_, a: &Self, b: Value| {
|
||||
Ok(borrow_angle(&b).map(|b| *a == b).unwrap_or(false))
|
||||
});
|
||||
m.add_meta_method("__tostring", |_, v: &Self, _: ()| {
|
||||
Ok(format!("{} {} {}", v.p, v.y, v.r))
|
||||
});
|
||||
m.add_function("Forward", |_, v: AnyUserData| {
|
||||
Ok(v.borrow::<Self>()?.forward())
|
||||
});
|
||||
m.add_function("Right", |_, v: AnyUserData| Ok(v.borrow::<Self>()?.right()));
|
||||
m.add_function("Up", |_, v: AnyUserData| Ok(v.borrow::<Self>()?.up()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Обнуляет числа, меньшие порога.
|
||||
///
|
||||
/// `cos(90°)` в f32 — это 6.1e-17, а не ноль. Если такое значение попадёт в
|
||||
/// позицию объекта, оно будет копиться в преобразованиях и «утечёт» в
|
||||
/// отрисовку, а `print` покажет `-0.00000004371139` вместо `0`.
|
||||
fn clean(value: f32) -> f32 {
|
||||
if value.abs() < 1e-6 { 0.0 } else { value }
|
||||
}
|
||||
|
||||
impl LuaAngle {
|
||||
/// Вектор «вперёд» для этих углов.
|
||||
///
|
||||
/// Source — **левая** система координат, поэтому знак Z уforward
|
||||
/// отрицательный: при нулевых углах игрок смотрит вдоль +X.
|
||||
pub fn forward(self) -> LuaVector {
|
||||
let (sp, cp) = self.p.to_radians().sin_cos();
|
||||
let (sy, cy) = self.y.to_radians().sin_cos();
|
||||
LuaVector::new(clean(cp * cy), clean(cp * sy), clean(-sp))
|
||||
}
|
||||
|
||||
/// Вектор «вправо» (вправо от взгляда).
|
||||
pub fn right(self) -> LuaVector {
|
||||
let (sp, cp) = self.p.to_radians().sin_cos();
|
||||
let (sy, cy) = self.y.to_radians().sin_cos();
|
||||
LuaVector::new(clean(-sp * sy), clean(sp * cy), clean(-cp))
|
||||
}
|
||||
|
||||
/// Вектор «вверх».
|
||||
pub fn up(self) -> LuaVector {
|
||||
let (sp, cp) = self.p.to_radians().sin_cos();
|
||||
let (sr, cr) = self.r.to_radians().sin_cos();
|
||||
LuaVector::new(clean(cr * sp), clean(-sr * cp), clean(cr * cp))
|
||||
}
|
||||
}
|
||||
|
||||
impl UserData for LuaColor {
|
||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("r", |_, c| Ok(c.r));
|
||||
fields.add_field_method_get("g", |_, c| Ok(c.g));
|
||||
fields.add_field_method_get("b", |_, c| Ok(c.b));
|
||||
fields.add_field_method_get("a", |_, c| Ok(c.a));
|
||||
fields.add_field_method_set("r", |_, c, value: u8| {
|
||||
c.r = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("g", |_, c, value: u8| {
|
||||
c.g = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("b", |_, c, value: u8| {
|
||||
c.b = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("a", |_, c, value: u8| {
|
||||
c.a = value;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
|
||||
m.add_meta_method("__eq", |_, a: &Self, b: Value| {
|
||||
Ok(borrow_color(&b).map(|b| *a == b).unwrap_or(false))
|
||||
});
|
||||
m.add_meta_method("__tostring", |_, c: &Self, _: ()| {
|
||||
Ok(format!("{} {} {} {}", c.r, c.g, c.b, c.a))
|
||||
});
|
||||
m.add_function("Unpack", |lua, c: AnyUserData| {
|
||||
let c = *c.borrow::<Self>()?;
|
||||
let table = lua.create_table()?;
|
||||
table.set(1, c.r)?;
|
||||
table.set(2, c.g)?;
|
||||
table.set(3, c.b)?;
|
||||
table.set(4, c.a)?;
|
||||
Ok(table)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Копия вектора из значения Lua.
|
||||
///
|
||||
/// В mlua 0.11 для userdata нет автоматического `FromLua`, только
|
||||
/// `AnyUserData::borrow`, поэтому чтение всегда идёт через него.
|
||||
/// Число из значения Lua: и `1.5` (`Number`), и `1` (`Integer`).
|
||||
///
|
||||
/// Важно: `Value::as_number()` в mlua 0.11 различает **только** `Number` и
|
||||
/// молча возвращает `None` на `Integer`, а LuaJIT отдаёт целые именно как
|
||||
/// `Integer`. Наивный `as_number()` терял все целочисленные аргументы:
|
||||
/// `Vector(1, 2, 3)` давал нулевой вектор, и это выглядело как «арифметика
|
||||
/// не работает», хотя дело было в чтении чисел.
|
||||
fn value_number(lua: &Lua, value: &Value) -> Option<f64> {
|
||||
match value {
|
||||
Value::Number(n) => Some(*n),
|
||||
Value::Integer(i) => Some(*i as f64),
|
||||
// Строка тоже: GMod-скрипты часто передают «1.5» из конфига.
|
||||
Value::String(_) => {
|
||||
let text: String = mlua::FromLua::from_lua(value.clone(), lua).ok()?;
|
||||
text.trim().parse::<f64>().ok()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn borrow_vector(value: &Value) -> Option<LuaVector> {
|
||||
Some(*value.as_userdata()?.borrow::<LuaVector>().ok()?)
|
||||
}
|
||||
|
||||
fn borrow_angle(value: &Value) -> Option<LuaAngle> {
|
||||
Some(*value.as_userdata()?.borrow::<LuaAngle>().ok()?)
|
||||
}
|
||||
|
||||
fn borrow_color(value: &Value) -> Option<LuaColor> {
|
||||
Some(*value.as_userdata()?.borrow::<LuaColor>().ok()?)
|
||||
}
|
||||
|
||||
/// Достаёт вектор из значения Lua: userdata или три числа.
|
||||
///
|
||||
/// GMod принимает `Vector(x, y, z)`, а внутри скриптов регулярно мешается
|
||||
/// `Vector(v)` с уже готовым вектором — оба случая обязаны работать.
|
||||
fn as_vector(lua: &Lua, value: &Value) -> Option<LuaVector> {
|
||||
if let Some(v) = borrow_vector(value) {
|
||||
return Some(v);
|
||||
}
|
||||
if !value.is_table() {
|
||||
return None;
|
||||
}
|
||||
let table: Table = value.as_table()?.clone();
|
||||
let get = |key: &str, default: f32| -> f32 {
|
||||
match table.get::<Value>(key) {
|
||||
Ok(value) => match value_number(lua, &value) {
|
||||
Some(n) if n.is_finite() => n as f32,
|
||||
_ => default,
|
||||
},
|
||||
Err(_) => default,
|
||||
}
|
||||
};
|
||||
Some(LuaVector::new(get("x", 0.0), get("y", 0.0), get("z", 0.0)))
|
||||
}
|
||||
|
||||
/// Скаляр из значения Lua: число, или 1 при не-числе.
|
||||
///
|
||||
/// Не-число трактуется как единица, а не как ошибка: в GMod `v * nil` даёт
|
||||
/// `v`, и падать на этом — ломать скрипты автора.
|
||||
fn as_scalar(value: &Value) -> f32 {
|
||||
// Без строк: `v * "2"` в GMod не определено, и трактовать это как 2 —
|
||||
// значит тихо менять смысл выражения.
|
||||
match value {
|
||||
Value::Number(n) if n.is_finite() => *n as f32,
|
||||
Value::Integer(i) => *i as f32,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn number(lua: &Lua, vector: LuaVector) -> mlua::Result<AnyUserData> {
|
||||
lua.create_userdata(vector)
|
||||
}
|
||||
|
||||
fn add(lua: &Lua, a: LuaVector, b: &Value) -> mlua::Result<AnyUserData> {
|
||||
match as_vector(lua, b) {
|
||||
Some(v) => number(lua, LuaVector::new(a.x + v.x, a.y + v.y, a.z + v.z)),
|
||||
None => {
|
||||
let s = as_scalar(b);
|
||||
number(lua, LuaVector::new(a.x + s, a.y + s, a.z + s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sub(lua: &Lua, a: LuaVector, b: &Value) -> mlua::Result<AnyUserData> {
|
||||
match as_vector(lua, b) {
|
||||
Some(v) => number(lua, LuaVector::new(a.x - v.x, a.y - v.y, a.z - v.z)),
|
||||
None => {
|
||||
let s = as_scalar(b);
|
||||
number(lua, LuaVector::new(a.x - s, a.y - s, a.z - s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mul(lua: &Lua, a: LuaVector, b: &Value) -> mlua::Result<AnyUserData> {
|
||||
match as_vector(lua, b) {
|
||||
// Покомпонентное умножение: так ведёт себя GMod.
|
||||
Some(v) => number(lua, LuaVector::new(a.x * v.x, a.y * v.y, a.z * v.z)),
|
||||
None => {
|
||||
let s = as_scalar(b);
|
||||
number(lua, LuaVector::new(a.x * s, a.y * s, a.z * s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn div(lua: &Lua, a: LuaVector, b: &Value) -> mlua::Result<AnyUserData> {
|
||||
match as_vector(lua, b) {
|
||||
// Деление покомпонентно, но ноль в знаменателе заменяем на 1: в GMod
|
||||
// деление на ноль даёт ноль, а у нас получился бы `inf`, который
|
||||
// потом отравляет всю геометрию пропа.
|
||||
Some(v) => number(
|
||||
lua,
|
||||
LuaVector::new(
|
||||
a.x / safe_divisor(v.x),
|
||||
a.y / safe_divisor(v.y),
|
||||
a.z / safe_divisor(v.z),
|
||||
),
|
||||
),
|
||||
None => {
|
||||
let s = as_scalar(b);
|
||||
let d = safe_divisor(s);
|
||||
number(lua, LuaVector::new(a.x / d, a.y / d, a.z / d))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_divisor(value: f32) -> f32 {
|
||||
if value.abs() < 1e-6 { 1.0 } else { value }
|
||||
}
|
||||
|
||||
/// Ставит конструкторы `Vector`, `Angle`, `Color` и `math.Clamp` в глобалы.
|
||||
pub fn install(lua: &Lua) -> Result<(), mlua::Error> {
|
||||
let globals = lua.globals();
|
||||
|
||||
globals.set(
|
||||
"Vector",
|
||||
lua.create_function(|lua, args: mlua::MultiValue| {
|
||||
let values: Vec<Option<f64>> = args.iter().map(|v| value_number(lua, v)).collect();
|
||||
let get = |i: usize, default: f32| -> f32 {
|
||||
values
|
||||
.get(i)
|
||||
.copied()
|
||||
.flatten()
|
||||
.filter(|v| v.is_finite())
|
||||
.map(|v| v as f32)
|
||||
.unwrap_or(default)
|
||||
};
|
||||
// `Vector()` без аргументов в GMod даёт нулевой вектор, а не
|
||||
// ошибку: это вызывается из кода, который сам подставляет значения.
|
||||
lua.create_userdata(LuaVector::new(get(0, 0.0), get(1, 0.0), get(2, 0.0)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
globals.set(
|
||||
"Angle",
|
||||
lua.create_function(|lua, args: mlua::MultiValue| {
|
||||
// `Angle(v)` с готовым углом обязан работать: так делает base_ents.
|
||||
if args.len() == 1 {
|
||||
if let Some(a) = args.front().and_then(borrow_angle) {
|
||||
return lua.create_userdata(a);
|
||||
}
|
||||
}
|
||||
let values: Vec<Option<f64>> = args.iter().map(|v| value_number(lua, v)).collect();
|
||||
let get = |i: usize| -> f32 {
|
||||
values
|
||||
.get(i)
|
||||
.copied()
|
||||
.flatten()
|
||||
.filter(|v| v.is_finite())
|
||||
.map(|v| v as f32)
|
||||
.unwrap_or(0.0)
|
||||
};
|
||||
lua.create_userdata(LuaAngle::new(get(0), get(1), get(2)))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
globals.set(
|
||||
"Color",
|
||||
lua.create_function(|lua, args: mlua::MultiValue| {
|
||||
if args.len() == 1 {
|
||||
if let Some(c) = args.front().and_then(borrow_color) {
|
||||
return lua.create_userdata(c);
|
||||
}
|
||||
}
|
||||
let values: Vec<Option<f64>> = args.iter().map(|v| value_number(lua, v)).collect();
|
||||
let get = |i: usize, default: u8| {
|
||||
LuaColor::component(values.get(i).copied().flatten(), default)
|
||||
};
|
||||
lua.create_userdata(LuaColor::new(
|
||||
get(0, 255),
|
||||
get(1, 255),
|
||||
get(2, 255),
|
||||
get(3, 255),
|
||||
))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `math.Clamp` — есть почти в каждом base_ents-скрипте.
|
||||
let math_table: Table = globals.get("math")?;
|
||||
math_table.set(
|
||||
"Clamp",
|
||||
lua.create_function(|_, (value, low, high): (f64, f64, f64)| Ok(value.clamp(low, high)))?,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::{Lua, Value};
|
||||
|
||||
/// Читает значение-вектор из глобальной переменной.
|
||||
fn get_vector(lua: &Lua, name: &str) -> LuaVector {
|
||||
let value = lua.globals().get::<Value>(name).expect("переменная");
|
||||
borrow_vector(&value).unwrap_or_else(|| panic!("{name} — не вектор"))
|
||||
}
|
||||
|
||||
fn get_angle(lua: &Lua, name: &str) -> LuaAngle {
|
||||
let value = lua.globals().get::<Value>(name).expect("переменная");
|
||||
borrow_angle(&value).unwrap_or_else(|| panic!("{name} — не угол"))
|
||||
}
|
||||
|
||||
fn get_color(lua: &Lua, name: &str) -> LuaColor {
|
||||
let value = lua.globals().get::<Value>(name).expect("переменная");
|
||||
borrow_color(&value).unwrap_or_else(|| panic!("{name} — не цвет"))
|
||||
}
|
||||
|
||||
/// Готовит рантайм с одним установленным модулем.
|
||||
fn eval(source: &str) -> Lua {
|
||||
let lua = Lua::new();
|
||||
install(&lua).expect("установка типов");
|
||||
lua.load(source).exec().expect("скрипт не выполнился");
|
||||
lua
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_arithmetic_works() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
a = Vector(1, 2, 3)
|
||||
b = Vector(10, 20, 30)
|
||||
sum = a + b
|
||||
diff = b - a
|
||||
scale = a * 2
|
||||
part = a * b
|
||||
"#,
|
||||
);
|
||||
let sum = get_vector(&lua, "sum");
|
||||
assert_eq!(sum, LuaVector::new(11.0, 22.0, 33.0));
|
||||
let diff = get_vector(&lua, "diff");
|
||||
assert_eq!(diff, LuaVector::new(9.0, 18.0, 27.0));
|
||||
let scale = get_vector(&lua, "scale");
|
||||
assert_eq!(scale, LuaVector::new(2.0, 4.0, 6.0));
|
||||
// Умножение векторов в GMod покомпонентное, а не скалярное.
|
||||
let part = get_vector(&lua, "part");
|
||||
assert_eq!(part, LuaVector::new(10.0, 40.0, 90.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_length_and_negation() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
v = Vector(3, 4, 0)
|
||||
len = #v
|
||||
neg = -v
|
||||
dist = v:Distance(Vector(0, 0, 0))
|
||||
dot = v:Dot(Vector(1, 0, 0))
|
||||
"#,
|
||||
);
|
||||
let globals = lua.globals();
|
||||
let len = <f32 as mlua::FromLua>::from_lua(globals.get("len").unwrap(), &lua).unwrap();
|
||||
assert!(
|
||||
(len - 5.0).abs() < 1e-4,
|
||||
"длина 3-4-0 равна 5, получили {len}"
|
||||
);
|
||||
let neg = get_vector(&lua, "neg");
|
||||
assert_eq!(neg, LuaVector::new(-3.0, -4.0, 0.0));
|
||||
let dist = <f32 as mlua::FromLua>::from_lua(globals.get("dist").unwrap(), &lua).unwrap();
|
||||
assert!((dist - 5.0).abs() < 1e-4);
|
||||
let dot = <f32 as mlua::FromLua>::from_lua(globals.get("dot").unwrap(), &lua).unwrap();
|
||||
assert!((dot - 3.0).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_fields_are_writable() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
v = Vector(1, 2, 3)
|
||||
v.x = 10
|
||||
v.z = -5
|
||||
"#,
|
||||
);
|
||||
let v = get_vector(&lua, "v");
|
||||
assert_eq!(v, LuaVector::new(10.0, 2.0, -5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_arguments_do_not_break_scripts() {
|
||||
// Обычный код автора вызывает `Vector()` и `Vector(1, 2)`; падать здесь
|
||||
// нельзя — это гасит бытовые скрипты Sandbox.
|
||||
let lua = eval(
|
||||
r#"
|
||||
zero = Vector()
|
||||
two = Vector(1, 2)
|
||||
neg = -two
|
||||
"#,
|
||||
);
|
||||
let _globals = lua.globals();
|
||||
let zero = get_vector(&lua, "zero");
|
||||
assert_eq!(zero, LuaVector::default());
|
||||
let two = get_vector(&lua, "two");
|
||||
assert_eq!(two, LuaVector::new(1.0, 2.0, 0.0));
|
||||
let neg = get_vector(&lua, "neg");
|
||||
assert_eq!(neg, LuaVector::new(-1.0, -2.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn division_by_zero_does_not_produce_infinity() {
|
||||
// `inf`, попав в позицию пропа, отравляет геометрию всего объекта.
|
||||
let lua = eval(
|
||||
r#"
|
||||
v = Vector(10, 10, 10) / 0
|
||||
w = Vector(1, 2, 3) / Vector(0, 2, 0)
|
||||
"#,
|
||||
);
|
||||
let _globals = lua.globals();
|
||||
let v = get_vector(&lua, "v");
|
||||
assert!(
|
||||
v.x.is_finite() && v.y.is_finite() && v.z.is_finite(),
|
||||
"{v:?}"
|
||||
);
|
||||
let w = get_vector(&lua, "w");
|
||||
assert!(w.x.is_finite(), "1/0 должно давать 1, а не inf: {w:?}");
|
||||
assert!((w.y - 1.0).abs() < 1e-5, "2/2 должно быть 1: {w:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tostring_matches_gmod_format() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
v = tostring(Vector(1, 2, 3))
|
||||
c = tostring(Color(255, 128, 0, 255))
|
||||
"#,
|
||||
);
|
||||
let globals = lua.globals();
|
||||
let v = <String as mlua::FromLua>::from_lua(globals.get("v").unwrap(), &lua).unwrap();
|
||||
assert_eq!(v, "1 2 3");
|
||||
let c = <String as mlua::FromLua>::from_lua(globals.get("c").unwrap(), &lua).unwrap();
|
||||
assert_eq!(c, "255 128 0 255");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_forward_follows_source_axes() {
|
||||
// Source — левая система координат: yaw = 90° смотрит вдоль +Y (влево).
|
||||
let lua = eval(
|
||||
r#"
|
||||
zero = Angle(0, 0, 0):Forward()
|
||||
left = Angle(0, 90, 0):Forward()
|
||||
"#,
|
||||
);
|
||||
let zero = get_vector(&lua, "zero");
|
||||
assert!(
|
||||
(zero.x - 1.0).abs() < 1e-4 && zero.y.abs() < 1e-4,
|
||||
"нулевые углы смотрят вдоль +X, получили {zero:?}"
|
||||
);
|
||||
let left = get_vector(&lua, "left");
|
||||
assert!(
|
||||
left.y.abs() > 0.99 && left.x.abs() < 1e-4,
|
||||
"yaw=90° должен смотреть вдоль +Y, получили {left:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_is_writable_and_comparable() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
a = Angle(0, 0, 0)
|
||||
b = Angle(0, 0, 0)
|
||||
a.y = 45
|
||||
same = (a == b)
|
||||
c = Angle(a)
|
||||
"#,
|
||||
);
|
||||
let globals = lua.globals();
|
||||
let a = get_angle(&lua, "a");
|
||||
assert!((a.y - 45.0).abs() < 1e-5);
|
||||
let same = <bool as mlua::FromLua>::from_lua(globals.get("same").unwrap(), &lua).unwrap();
|
||||
assert!(!same, "после записи y углы должны различаться");
|
||||
// `Angle(a)` обязан копировать, а не терять значение.
|
||||
let c = get_angle(&lua, "c");
|
||||
assert!(
|
||||
(c.y - 45.0).abs() < 1e-5,
|
||||
"копия должна сохранять углы: {c:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_components_are_clamped_not_rejected() {
|
||||
let lua = eval(
|
||||
r#"
|
||||
hot = Color(1.5 * 300, 0, 0)
|
||||
thin = Color(0, 0, 0, 128)
|
||||
"#,
|
||||
);
|
||||
let _globals = lua.globals();
|
||||
let hot = get_color(&lua, "hot");
|
||||
assert_eq!(hot.r, 255, "компонента вне 0..255 обрезается, а не падает");
|
||||
let thin = get_color(&lua, "thin");
|
||||
assert_eq!(thin.a, 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nan_from_script_cannot_enter_the_type() {
|
||||
// `0/0` в Lua даёт `nan`, и он обязан не доехать до физики.
|
||||
let lua = eval(
|
||||
r#"
|
||||
bad = Color(0/0, 0/0, 0/0, 0/0)
|
||||
v = Vector(0/0, 1, 2)
|
||||
"#,
|
||||
);
|
||||
let _globals = lua.globals();
|
||||
let bad = get_color(&lua, "bad");
|
||||
assert_eq!(
|
||||
bad,
|
||||
LuaColor::default(),
|
||||
"nan должен заменяться значением по умолчанию"
|
||||
);
|
||||
let v = get_vector(&lua, "v");
|
||||
assert!(v.x.is_finite(), "nan не должен попадать в вектор");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn math_clamp_is_available() {
|
||||
let lua = eval("v = math.Clamp(15, 0, 10)");
|
||||
let v = <f64 as mlua::FromLua>::from_lua(lua.globals().get("v").unwrap(), &lua).unwrap();
|
||||
assert_eq!(v, 10.0);
|
||||
}
|
||||
|
||||
/// Строковые представления должны читаться как в GMod — на этом завязан
|
||||
/// `ErrorNoHud` и текст ошибок в консоли.
|
||||
#[test]
|
||||
fn tostring_works_through_global_tostring() {
|
||||
let lua = eval("s = tostring(Vector(1, 2, 3))");
|
||||
let s = <String as mlua::FromLua>::from_lua(lua.globals().get("s").unwrap(), &lua)
|
||||
.expect("строка");
|
||||
assert_eq!(s, "1 2 3");
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
//! v0: `print`/`Msg`, `hook.*`, `timer.Simple`, `concommand.*` (реестр),
|
||||
//! вывод в буфер для HUD/консоли. Игровые библиотеки — следующие итерации.
|
||||
|
||||
pub mod gmod;
|
||||
|
||||
use mlua::{Function, Lua, MultiValue, RegistryKey, Table, Value};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
|
@ -33,6 +35,7 @@ impl LuaRuntime {
|
|||
let output: Output = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
install_api(&lua, &timers, &output)?;
|
||||
gmod::install(&lua)?;
|
||||
|
||||
Ok(Self {
|
||||
lua,
|
||||
|
|
@ -636,4 +639,37 @@ local t = [[raw ! text]]
|
|||
assert!(rt.run_command("sr_test", "").expect("вызов"));
|
||||
assert!(rt.take_output().iter().any(|l| l == "ran"));
|
||||
}
|
||||
|
||||
/// Типы GMod должны работать **вместе** с остальным API: скрипт,
|
||||
/// который печатает вектор через `print`, обязан печатать и через
|
||||
/// `Msg`, иначе типы работают только в вакууме.
|
||||
#[test]
|
||||
fn gmod_types_work_inside_the_real_runtime() {
|
||||
let runtime = super::LuaRuntime::new().expect("рантайм");
|
||||
runtime
|
||||
.run(
|
||||
"types.lua",
|
||||
br#"
|
||||
a = Vector(1, 2, 3)
|
||||
b = Vector(10, 20, 30)
|
||||
print(tostring(a + b))
|
||||
print(tostring(#(a + b)))
|
||||
print(tostring(Angle(0, 90, 0):Forward()))
|
||||
print(tostring(Color(255, 128, 0, 255)))
|
||||
local v = Vector(0, 0, 0)
|
||||
v.x = 5
|
||||
print(tostring(v))
|
||||
print(tostring(math.Clamp(42, 0, 10)))
|
||||
"#,
|
||||
)
|
||||
.expect("скрипт выполнился");
|
||||
let out = runtime.take_output();
|
||||
assert_eq!(out[0], "11 22 33", "сложение векторов");
|
||||
// sqrt(11² + 22² + 33²) = sqrt(1694) = 41.1582…
|
||||
assert_eq!(out[1], "41.158229827881", "длина (11,22,33)");
|
||||
assert_eq!(out[2], "0 1 0", "yaw=90° смотрит вдоль +Y");
|
||||
assert_eq!(out[3], "255 128 0 255", "цвет печатается как в GMod");
|
||||
assert_eq!(out[4], "5 0 0", "поле записывается");
|
||||
assert_eq!(out[5], "10", "math.Clamp");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,3 +451,63 @@ Rapier обслуживает только **динамические тела**
|
|||
- Брашевые `func_door` / `func_button` как ограничивающие тела.
|
||||
- `env_explosion` через `apply_explosion` (метод готов, вызов из Lua — нет).
|
||||
- Замер FPS с физикой и без, чтобы понять цену 2 121 коллайдера.
|
||||
|
||||
## Часть D1. Lua-типы GMod (сделано)
|
||||
|
||||
### D1. Что сделано
|
||||
|
||||
`crates/lua/src/gmod.rs` — `Vector`, `Angle`, `Color` как userdata с
|
||||
арифметикой, полями на запись и `tostring` в формате GMod. Установлены в
|
||||
глобалы вместе с `math.Clamp`.
|
||||
|
||||
Решения, важные для совместимости:
|
||||
|
||||
- **Пропущенных аргументов не бывает.** `Vector()` и `Vector(1, 2)` обязаны
|
||||
работать: так вызывается бытовой код Sandbox. Отсутствующее значение
|
||||
заменяется нулём, а не ошибкой.
|
||||
- **Деление на ноль даёт 1, а не `inf`.** `inf` в позиции пропа отравляет
|
||||
геометрию всего объекта и потом всплывает в отрисовке.
|
||||
- **NaN из скрипта не доезжает до физики.** `Color(0/0, ...)` даёт белый,
|
||||
`Vector(0/0, 1, 2)` — вектор без NaN.
|
||||
- **Компоненты цвета обрезаются в 0..255**, а не отвергаются:
|
||||
`Color(1.5 * 300, 0, 0)` — обычное дело в скриптах.
|
||||
- **Source — левая система координат**: `Angle(0,0,0):Forward()` смотрит вдоль
|
||||
`+X`, `Angle(0,90,0):Forward()` — вдоль `+Y`, знак Z у forward
|
||||
отрицательный.
|
||||
|
||||
### D1.1. Дефект чтения чисел в mlua 0.11
|
||||
|
||||
`Value::as_number()` в mlua 0.11 различает **только** `Value::Number` и молча
|
||||
возвращает `None` на `Value::Integer`. LuaJIT отдаёт целые именно как
|
||||
`Integer`, поэтому наивное чтение теряло **все** целочисленные аргументы:
|
||||
|
||||
Vector(1, 2, 3) --> Vector(0, 0, 0)
|
||||
|
||||
Выглядело это как «арифметика не работает», хотя дело было в чтении чисел.
|
||||
Собственный `value_number()` принимает `Number`, `Integer` и строку (числом).
|
||||
Тест на это обязателен: без него `Vector()` с целыми просто тихо отдаёт нули.
|
||||
|
||||
### D1.2. Шум тригонометрии
|
||||
|
||||
`cos(90°)` в f32 — это 6.1e-17, а не ноль. Без обнуления
|
||||
`Angle(0, 90, 0):Forward()` печатает `-0.00000004371139 1 -0`, и такие
|
||||
значения копятся в преобразованиях координат. Порог — 1e-6.
|
||||
|
||||
### D1.3. Особенности API mlua 0.11, на которые ушло время
|
||||
|
||||
- Для userdata **нет** автоматического `FromLua`: чтение только через
|
||||
`AnyUserData::borrow::<T>()`. `Lua::from_value` отсутствует, есть
|
||||
`FromLua::from_lua(value, lua)`.
|
||||
- Поля — `add_fields` (трейт `UserDataFields`), методы и метаметоды —
|
||||
`add_methods` (трейт `UserDataMethods`). Один трейт на всё не работает.
|
||||
- `add_meta_method` получает `(&Lua, &T, A)`: значение объекта идёт вторым
|
||||
аргументом, лишние аргументы Lua (`__unm(a, a)`) отбрасываются через `()`.
|
||||
- `add_function` получает объект как `AnyUserData` первым элементом кортежа.
|
||||
|
||||
### D1.4. Что осталось по части D1
|
||||
|
||||
- `Entity()` и объект сущности: `GetPos`/`SetPos`/`GetClass`/`IsValid`.
|
||||
- `ents.*`: `Create`, `GetAll`, `FindInSphere`, `GetPlayer`.
|
||||
- Связка с движком: `glua` не должен зависеть от `engine`, обмен — через
|
||||
колбэки, которые регистрирует клиент.
|
||||
- `Vector:Angle()`, `Vector:Distance` между сущностями.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue