Замер коллизий по всей карте: мнимых стен в кирпичах нет
Some checks are pending
CI / checks (push) Waiting to run
Some checks are pending
CI / checks (push) Waiting to run
Три диагностики, чтобы перестать гадать по жалобам про «невидимые стены». `probe_ramp_movement` — движение у рампы из точки пользователя (`-2438 -966 48`): роняет игрока на опору, гоняет восемь сторон и на каждом кадре сравнивает сдвиг с ожидаемым. Рывков нет, залипания есть, и виновник найден: кирпич #829 по `y = -1024` с настоящими текстурами бетона и кирпича — стена видна на кадре из этой точки. Фантомного выталкивания в точке нет. `probe_phantom_walls_scan` — обход всей карты (11200 точек, 10824 свободных) с шагом кадра 8 юнитов ищет кирпич, который отсекает шаг, находясь дальше 8 юнитов от тела, и отдельно — ложное «тело внутри, хотя по плоскостям снаружи». Оба счётчика нулевые. Это объясняет, почему старый обход молчал: его сетка покрывала только центр (x −1500..600, y −800..600), запад и юг карты не проверялись вовсе. `probe_collision_contents` — состав коллизии по данным BSP: 2118 кирпичей из 2203, и служебных среди них нет. 7 кирпичей `func_vehicleclip` (в Source режут только машины) исключены владельцем, триггеры — тоже; ни TOOLSTRIGGER, ни клипов в коллизии не осталось. Текстуры кирпичей читаются прямо из `dbrushside.texinfo`, а не подбором по плоскостям. Вывод: у «невидимых стен» вокруг рамп и стыков источник не в наборе кирпичей. Нужен точный адрес места, где это чувствуется: отладочный слой из прошлого коммита покажет, есть ли у мнимой стены рёбра кирпича вообще. В docs/PARITY.md A9 дописаны все три замера и остаток вопроса.
This commit is contained in:
parent
86a6cb63f9
commit
5380184dfe
2 changed files with 277 additions and 0 deletions
|
|
@ -4376,6 +4376,263 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// Обход всей карты в поисках «невидимых стен»: кирпич блокирует шаг,
|
||||
/// но не достаёт до тела.
|
||||
///
|
||||
/// Старая сетка обхода (`real_map_sweep_for_invisible_walls`) покрывала
|
||||
/// только центр: `x` от −1500 до 600, `y` от −800 до 600. Запад, юг и
|
||||
/// восток карты не проверялись вовсе — а жалобы приходят именно оттуда
|
||||
/// (`-2438 -966 48`). Здесь сетка по **габаритам всей карты**: сначала
|
||||
/// отбираются точки, где тело ничего не касается (свободный объём), затем
|
||||
/// в каждой делается шаг в восемь сторон, и ищется кирпич, который
|
||||
/// отсекает движение, находясь дальше 8 юнитов от тела. Это и есть
|
||||
/// невидимая стена: настоящая стена стоит вплотную, а такая — в пустоте.
|
||||
///
|
||||
/// Шаг сетки задаётся `SOURCE_RS_SCAN_STEP` (по умолчанию 400).
|
||||
#[test]
|
||||
#[ignore = "диагностика: SOURCE_RS_TEST_BSP"]
|
||||
fn probe_phantom_walls_scan() {
|
||||
let Some(path) = std::env::var_os("SOURCE_RS_TEST_BSP") else {
|
||||
eprintln!("SOURCE_RS_TEST_BSP не задан — проверка пропущена");
|
||||
return;
|
||||
};
|
||||
let data = std::fs::read(&path).expect("BSP читается");
|
||||
let bsp = Bsp::from_bytes(data).expect("BSP разбирается");
|
||||
let collision = Collision::from_bsp(&bsp).expect("коллизия строится");
|
||||
let world = crate::world::build_world(&bsp, &fs::VirtualFs::new()).expect("мир строится");
|
||||
let step: f32 = std::env::var("SOURCE_RS_SCAN_STEP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(400.0);
|
||||
|
||||
// Расстояние между габаритами: ноль, если они пересекаются или
|
||||
// соприкасаются.
|
||||
fn separation(a: &Aabb, b: &Aabb) -> f32 {
|
||||
let axis = |a_min: f32, a_max: f32, b_min: f32, b_max: f32| -> f32 {
|
||||
(a_min - b_max).max(b_min - a_max).max(0.0)
|
||||
};
|
||||
Vec3::new(
|
||||
axis(a.min.x, a.max.x, b.min.x, b.max.x),
|
||||
axis(a.min.y, a.max.y, b.min.y, b.max.y),
|
||||
axis(a.min.z, a.max.z, b.min.z, b.max.z),
|
||||
)
|
||||
.length()
|
||||
}
|
||||
|
||||
let mut samples = 0usize;
|
||||
let mut free = 0usize;
|
||||
let mut phantom: Vec<(usize, Vec3, f32)> = Vec::new();
|
||||
let mut x = world.bounds_min.x + step * 0.5;
|
||||
while x < world.bounds_max.x {
|
||||
let mut y = world.bounds_min.y + step * 0.5;
|
||||
while y < world.bounds_max.y {
|
||||
let mut z = world.bounds_max.z - 64.0;
|
||||
while z > world.bounds_min.z + 64.0 {
|
||||
samples += 1;
|
||||
let center = Vec3::new(x, y, z);
|
||||
let body = Aabb::new(center - STAND_HALF, center + STAND_HALF);
|
||||
if collision.touching(&body) {
|
||||
z -= step * 0.5;
|
||||
continue;
|
||||
}
|
||||
free += 1;
|
||||
// Кирпич считает тело внутри (`penetration`), хотя по
|
||||
// полупространствам тело вышло за грань: это ложное
|
||||
// перекрытие, и `resolve_overlap` вытолкнет тело из
|
||||
// пустоты. Шаг движения тут не нужен вовсе.
|
||||
for (index, brush) in collision.brushes.iter().enumerate() {
|
||||
if !brush.bounds.min.is_finite() || brush.planes.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
// Тело вне кирпича, если вышло хотя бы за одну грань.
|
||||
let outside = brush.planes.iter().any(|(normal, dist)| {
|
||||
let inflate = normal.x.abs() * STAND_HALF.x
|
||||
+ normal.y.abs() * STAND_HALF.y
|
||||
+ normal.z.abs() * STAND_HALF.z;
|
||||
normal.dot(center) + inflate > dist + 0.01
|
||||
});
|
||||
if !outside {
|
||||
continue;
|
||||
}
|
||||
if let Some((depth, direction)) = Collision::penetration(brush, &body) {
|
||||
let gap = separation(&body, &brush.bounds);
|
||||
phantom.push((index, center, depth.max(gap)));
|
||||
let _ = direction;
|
||||
break;
|
||||
}
|
||||
}
|
||||
z -= step * 0.5;
|
||||
}
|
||||
y += step;
|
||||
}
|
||||
x += step;
|
||||
}
|
||||
|
||||
let mut brushes: Vec<usize> = phantom.iter().map(|(index, _, _)| *index).collect();
|
||||
brushes.sort_unstable();
|
||||
brushes.dedup();
|
||||
println!(
|
||||
"ЗАМЕР обход всей карты: точек {samples}, свободных {free}, \
|
||||
невидимых стен {} у {} кирпичей",
|
||||
phantom.len(),
|
||||
brushes.len()
|
||||
);
|
||||
for (index, center, gap) in phantom.iter().take(12) {
|
||||
let brush = &collision.brushes[*index];
|
||||
println!(
|
||||
"ЗАМЕР кирпич #{index} (граней {}, габарит {:?}..{:?}) держит тело в \
|
||||
{center:?}, зазор {gap:.1}",
|
||||
brush.planes.len(),
|
||||
brush.bounds.min,
|
||||
brush.bounds.max
|
||||
);
|
||||
}
|
||||
println!("ЗАМЕР кирпичи: {brushes:?}");
|
||||
}
|
||||
|
||||
/// Что за кирпичи попадают в коллизию: содержимое и текстуры граней.
|
||||
///
|
||||
/// Жалоба: «невидимые стены, которые можно перепрыгнуть, а запрыгнуть на
|
||||
/// них нельзя» — так ведёт себя **служебный** браш: клип, триггер,
|
||||
/// `nodraw`. У `dbrushside_t` есть свой `texinfo`, поэтому текстуру кирпича
|
||||
/// видно прямо из данных, без сопоставления плоскостей с гранями.
|
||||
#[test]
|
||||
#[ignore = "диагностика: SOURCE_RS_TEST_BSP"]
|
||||
fn probe_collision_contents() {
|
||||
let Some(path) = std::env::var_os("SOURCE_RS_TEST_BSP") else {
|
||||
eprintln!("SOURCE_RS_TEST_BSP не задан — проверка пропущена");
|
||||
return;
|
||||
};
|
||||
let data = std::fs::read(&path).expect("BSP читается");
|
||||
let bsp = Bsp::from_bytes(data).expect("BSP разбирается");
|
||||
let geometry = bsp.geometry().expect("геометрия");
|
||||
let texdata = bsp.texdata().expect("texdata");
|
||||
let names = bsp.texture_names().expect("имена текстур");
|
||||
let brush_data = bsp.lump(lump::BRUSHES).expect("кирпичи").into_owned();
|
||||
let side_data = bsp.lump(lump::BRUSHSIDES).expect("грани").into_owned();
|
||||
let entities = bsp.entities().unwrap_or_default();
|
||||
|
||||
let texture_of = |texinfo: i16| -> String {
|
||||
geometry
|
||||
.texinfos
|
||||
.get(texinfo.max(0) as usize)
|
||||
.and_then(|t| texdata.get(t.texdata.max(0) as usize))
|
||||
.and_then(|t| names.get(t.name_id.max(0) as usize))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
// Содержимое включённых кирпичей и их текстуры: «служебные» браши
|
||||
// узнаются по имени текстуры без всяких догадок.
|
||||
let mut contents_count: std::collections::HashMap<i32, usize> =
|
||||
std::collections::HashMap::new();
|
||||
let mut textures_count: std::collections::HashMap<String, usize> =
|
||||
std::collections::HashMap::new();
|
||||
let mut included = 0usize;
|
||||
for chunk in brush_data.chunks_exact(12) {
|
||||
let first = i32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0; 4]));
|
||||
let count = i32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0; 4]));
|
||||
let contents = i32::from_le_bytes(chunk[8..12].try_into().unwrap_or([0; 4]));
|
||||
let liquid = contents::is_liquid(contents);
|
||||
if !liquid && !contents::is_solid(contents) {
|
||||
continue;
|
||||
}
|
||||
included += 1;
|
||||
*contents_count.entry(contents).or_default() += 1;
|
||||
if first < 0 || count < 0 || count == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut textures: Vec<String> = Vec::new();
|
||||
for s in first as usize..(first + count) as usize {
|
||||
let Some(side) = side_data.get(s * 8..s * 8 + 8) else {
|
||||
break;
|
||||
};
|
||||
let texinfo = i16::from_le_bytes([side[2], side[3]]);
|
||||
let name = texture_of(texinfo);
|
||||
if !textures.contains(&name) {
|
||||
textures.push(name);
|
||||
}
|
||||
}
|
||||
for name in textures {
|
||||
*textures_count.entry(name).or_default() += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Служебные кирпичи поимённо: индекс, содержимое, владелец и текстуры.
|
||||
// Владелец — модель кирпича; у моделей есть сущность с `model *N`,
|
||||
// по ней и берём `classname`.
|
||||
let mut owner_model: std::collections::HashMap<u32, usize> =
|
||||
std::collections::HashMap::new();
|
||||
for (number, model) in geometry.models.iter().enumerate() {
|
||||
for index in Collision::brushes_under(&geometry, model.head_node) {
|
||||
owner_model.insert(index, number);
|
||||
}
|
||||
}
|
||||
let class_of_model = |number: usize| -> String {
|
||||
let wanted = format!("*{number}");
|
||||
entities
|
||||
.iter()
|
||||
.find(|entity| entity.get_str("model") == Some(wanted.as_str()))
|
||||
.and_then(|entity| entity.get_str("classname"))
|
||||
.unwrap_or("<world>")
|
||||
.to_string()
|
||||
};
|
||||
for (index, chunk) in brush_data.chunks_exact(12).enumerate() {
|
||||
let first = i32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0; 4]));
|
||||
let count = i32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0; 4]));
|
||||
let contents = i32::from_le_bytes(chunk[8..12].try_into().unwrap_or([0; 4]));
|
||||
let liquid = contents::is_liquid(contents);
|
||||
if first < 0 || count < 0 || (!liquid && !contents::is_solid(contents)) {
|
||||
continue;
|
||||
}
|
||||
let mut textures: Vec<String> = Vec::new();
|
||||
for s in first as usize..(first + count) as usize {
|
||||
let Some(side) = side_data.get(s * 8..s * 8 + 8) else {
|
||||
break;
|
||||
};
|
||||
let name = texture_of(i16::from_le_bytes([side[2], side[3]]));
|
||||
if !textures.contains(&name) {
|
||||
textures.push(name);
|
||||
}
|
||||
}
|
||||
let service = textures.iter().any(|name| {
|
||||
let upper = name.to_ascii_uppercase();
|
||||
upper.starts_with("TOOLS/") && !upper.contains("NODRAW")
|
||||
});
|
||||
if !service {
|
||||
continue;
|
||||
}
|
||||
let model = owner_model.get(&(index as u32)).copied().unwrap_or(0);
|
||||
println!(
|
||||
"ЗАМЕР кирпич #{index}: содержимое {contents:#010x}, владелец {}, \
|
||||
граней {count}, текстуры {textures:?}",
|
||||
class_of_model(model)
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"ЗАМЕР коллизия: кирпичей всего {}, в коллизию попало {included}",
|
||||
brush_data.len() / 12
|
||||
);
|
||||
let mut contents: Vec<(i32, usize)> = contents_count.into_iter().collect();
|
||||
contents.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
|
||||
for (value, count) in contents.iter().take(10) {
|
||||
println!("ЗАМЕР содержимое {value:#010x}: {count}");
|
||||
}
|
||||
// Мусорные и служебные текстуры — то, чего в глазной картинке нет.
|
||||
let mut textures: Vec<(String, usize)> = textures_count.into_iter().collect();
|
||||
textures.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
|
||||
for (name, count) in textures.iter().take(25) {
|
||||
println!("ЗАМЕР текстура {name:?}: {count}");
|
||||
}
|
||||
let tools: Vec<(&String, usize)> = textures
|
||||
.iter()
|
||||
.filter(|(name, _)| name.to_ascii_uppercase().contains("TOOLS/"))
|
||||
.map(|(name, count)| (name, *count))
|
||||
.collect();
|
||||
println!("ЗАМЕР служебные текстуры (TOOLS/): {tools:?}");
|
||||
}
|
||||
|
||||
/// Диагностика движения у рампы: рывки и залипания.
|
||||
///
|
||||
/// Жалоба: «я как будто стою вписанным в невидимый параллелепипед, а не
|
||||
|
|
|
|||
|
|
@ -215,6 +215,26 @@ displacement-геометрия, у которой **не разрешилась
|
|||
В пустых углах около рамп получается ложное перекрытие, и `resolve_overlap`
|
||||
выталкивает игрока из объёма, которого нет.
|
||||
|
||||
**Что уже замерено (2026-10-16):**
|
||||
|
||||
- `probe_ramp_movement` в точке `-2438 -966 48`: залипание при движении в −Y и
|
||||
по диагоналям объясняется **настоящей кирпичной стеной** — кирпич #829 стоит
|
||||
по `y = -1024`, текстуры бетон/кирпич, стена видна на кадре из этой точки.
|
||||
Фантомного выталкивания в этой точке нет.
|
||||
- `probe_phantom_walls_scan` — обход **всей карты** (11200 точек по габаритам,
|
||||
10824 свободных) с шагом кадра 8 юнитов: кирпичей, блокирующих шаг издалека,
|
||||
**ноль**. Ложных «тело внутри, хотя по плоскостям снаружи» — тоже ноль.
|
||||
- `probe_collision_contents` — состав коллизии: 2118 кирпичей из 2203 сырых;
|
||||
исключены 7 `func_vehicleclip` (в Source режут только машины), триггеры и
|
||||
прочие служебные; в коллизии не осталось ни `TOOLSTRIGGER`, ни клипов.
|
||||
- Причина, по которой старый обход молчал: его сетка покрывала только центр
|
||||
(`x` −1500..600, `y` −800..600), то есть запад и юг карты не проверялись.
|
||||
|
||||
**Что осталось:** нужен точный адрес «стены на стыке комнат» — на рампе такой
|
||||
стены нет. Отладочный слой (`SOURCE_RS_DEBUG_COLLISION=1`) покажет это прямо
|
||||
на экране: если у мнимой стены нет ни красных рёбер, ни жёлтого габарита —
|
||||
она не из кирпичей вовсе, и это отдельный механизм.
|
||||
|
||||
**Приёмка:** игрок в пустом углу рядом с рампой не сдвигается; обход карты с
|
||||
сеткой, покрывающей запад (`x` до −3100), без залипаний.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue