diff --git a/crates/client/src/map.rs b/crates/client/src/map.rs index 4ec886a..b6f7e4e 100644 --- a/crates/client/src/map.rs +++ b/crates/client/src/map.rs @@ -101,6 +101,10 @@ pub struct MapArgs { #[arg(long, value_name = "X Y Z")] pub pos: Option, + /// Заспавнить проп на старте: путь к модели (отладка спавна) + #[arg(long, value_name = "MODEL")] + pub spawn: Vec, + /// Позиция игрока «x y z» (отладка; по умолчанию — спавн) #[arg(long, value_name = "X Y Z")] pub player_pos: Option, @@ -144,6 +148,7 @@ impl MapArgs { speed: 400.0, pos: None, player_pos: None, + spawn: Vec::new(), angles: None, debug_materials: false, only_material: None, @@ -233,6 +238,20 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { index }); + // `--spawn <модель>` — спавн пропов на старте (отладка Q-меню, A24). + for model in &args.spawn { + let origin = player.eye_position() + math::Vec3::new(1.0, 0.0, 0.0) * 96.0; + let angles = math::Vec3::new(0.0, 0.0, 0.0); + let mut budget = usize::MAX; + match world + .borrow_mut() + .spawn_prop(&vfs, model, origin, angles, 10.0, &mut budget) + { + Some(index) => info!(index, model = %model, "проп заспавнен на старте"), + None => warn!(model = %model, "проп на старте не заспавнен"), + } + } + if let Some(dir) = &args.dump_textures { std::fs::create_dir_all(dir)?; for (index, material) in world.borrow().materials.iter().enumerate() { @@ -377,6 +396,48 @@ pub fn map(assets: Option, args: MapArgs) -> anyhow::Result<()> { keycode: Some(Keycode::Escape), .. } => break 'running, + Event::KeyDown { + keycode: Some(Keycode::G), + repeat: false, + .. + } => { + // Спавн пропа по взгляду — первый шаг к Q-меню (A24). + // Модель задаётся `SOURCE_RS_SPAWN_MODEL`, по умолчанию + // классический арбуз Sandbox. + let model = std::env::var("SOURCE_RS_SPAWN_MODEL") + .unwrap_or_else(|_| "models/props_junk/watermelon01.mdl".to_string()); + let origin = player.eye_position() + camera.forward() * 96.0; + let angles = math::Vec3::new(0.0, camera.yaw, 0.0); + let mut budget = usize::MAX; + let spawned = world.borrow_mut().spawn_prop( + &vfs, + &model, + origin, + angles, + 10.0, + &mut budget, + ); + match spawned { + Some(index) => { + info!(index, model = %model, "проп заспавнен"); + log.push_back(format!("spawn: {model}")); + // Буферы рендера собраны на сборке мира: чтобы + // новый проп было видно, пересобираем рендерер. + // Позже — до-аллокация вершин под спавн. + match WorldRenderer::new(&gfx, &world.borrow(), args.debug_materials) { + Ok(fresh) => renderer = fresh, + Err(err) => warn!("рендерер не пересобран: {err:#}"), + } + } + None => { + warn!(model = %model, "проп не заспавнен"); + log.push_back(format!("spawn не удался: {model}")); + } + } + while log.len() > 8 { + log.pop_front(); + } + } Event::KeyDown { keycode: Some(Keycode::V), repeat: false, diff --git a/crates/engine/src/world.rs b/crates/engine/src/world.rs index 74fa89a..01098dc 100644 --- a/crates/engine/src/world.rs +++ b/crates/engine/src/world.rs @@ -2254,14 +2254,19 @@ impl World { /// Путь тот же, что у пропов при сборке мира: модель с диска, материалы с /// дедупликацией по имени, вершины с трансформацией, батчи на материалы. /// Дальше экземпляр двигается `move_model_instance`. - pub fn spawn_model_instance( + /// Добавляет геометрию модели в мир: меш, материалы, вершины, батчи. + /// + /// Возвращает (индекс меша, первый индекс вершины, сколько вершин) — + /// этим пользуются и экземпляры без сущности (модель игрока, вьюмодель), + /// и спавн пропов на ходу. + pub fn append_model_geometry( &mut self, vfs: &VirtualFs, model_name: &str, origin: Vec3, angles: Vec3, budget: &mut usize, - ) -> Option { + ) -> Option<(usize, u32, u32)> { let key = crate::model::normalize_model_name(model_name); let model = crate::model::load_model(vfs, &key, budget).ok()?; self.prop_meshes @@ -2317,8 +2322,6 @@ impl World { if to > model.indices.len() { continue; } - // Индексы модели локальные — смещаем на вершины экземпляра в - // общем буфере мира, как это делает сборка пропов. self.indices .extend(model.indices[from..to].iter().map(|i| first_vertex + i)); let index_count = self.indices.len() as u32 - first_index; @@ -2328,8 +2331,22 @@ impl World { index_count, }); } + Some((mesh, first_vertex, vertex_count)) + } + + /// Добавляет экземпляр модели без сущности (модель игрока, вьюмодель). + pub fn spawn_model_instance( + &mut self, + vfs: &VirtualFs, + model_name: &str, + origin: Vec3, + angles: Vec3, + budget: &mut usize, + ) -> Option { + let (mesh, first_vertex, vertex_count) = + self.append_model_geometry(vfs, model_name, origin, angles, budget)?; self.model_instances.push(ModelInstance { - model: key, + model: crate::model::normalize_model_name(model_name), mesh, origin, angles, @@ -2339,6 +2356,60 @@ impl World { Some(self.model_instances.len() - 1) } + /// Спавнит проп на ходу — то, что делает Q-меню спавна в GMod. + /// + /// Путь тот же, что у пропов карты: геометрия модели, сущность класса + /// `prop_physics` (её видят Lua и физика), тело в мире физики по габаритам + /// меша и экземпляр пропа для `move_prop`. + pub fn spawn_prop( + &mut self, + vfs: &VirtualFs, + model_name: &str, + origin: Vec3, + angles: Vec3, + mass: f32, + budget: &mut usize, + ) -> Option { + let (mesh, first_vertex, vertex_count) = + self.append_model_geometry(vfs, model_name, origin, angles, budget)?; + + let mut entity = crate::entity::Entity::new("prop_physics".to_string()); + entity.model = Some(model_name.to_string()); + entity.origin = origin; + entity.angles = angles; + let entity_index = self.entities.spawn(entity)?; + + let half = self.prop_meshes[mesh].bounds_half_extents; + let physics_body = if half.length_squared() > 1.0 { + match self + .physics + .add_prop(entity_index, origin, angles, half, mass.max(0.1)) + { + Ok(body) => Some(body), + Err(err) => { + tracing::warn!(model = %model_name, "тело пропа не создано: {err}"); + None + } + } + } else { + None + }; + + self.props.push(PropInstance { + entity: entity_index, + classname: "prop_physics".to_string(), + model: crate::model::normalize_model_name(model_name), + mesh, + origin, + angles, + first_vertex, + vertex_count, + physics_body, + }); + self.update_prop_bounds(); + Some(self.props.len() - 1) + } + /// Двигает экземпляр модели без сущности. pub fn move_model_instance(&mut self, index: usize, origin: Vec3, angles: Vec3) -> bool { let Some(instance) = self.model_instances.get(index) else { diff --git a/docs/DIARY.md b/docs/DIARY.md index 8d8db8c..c030c5c 100644 --- a/docs/DIARY.md +++ b/docs/DIARY.md @@ -46,8 +46,20 @@ SOURCE_RS_PLAYER_MODEL= DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client - при вставке в общий буфер к ним обязательно прибавляется `first_vertex` (без этого был мусор в кадре). -**Следующее:** (а) вьюмодель в первом лице и скрытие своей модели, когда камера -внутри неё; (б) реестр SWEP из Lua; (в) физган; (г) скелет+анимации моделей. +**Спавн пропа из игры сделан (ядро A24).** `World::spawn_prop` (геометрия из +`append_model_geometry`, сущность `prop_physics`, тело в физике, экземпляр +пропа) + в клиенте клавиша `G` (спавн по взгляду) и аргумент +`--spawn <модель>` (отладка и скрипты). Проверено кадром: арбуз заспавнен и +отрисован (`--spawn models/props_junk/watermelon01.mdl`, камера у +`-2500 -1050 60|5 0`). + +Грабли: буферы рендера созданы на сборке мира — после спавна рендерер +пересобирается целиком (медленно, но работает); позже нужна до-аллокация +вершин под спавн. + +**Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера; +(б) реестр SWEP из Lua и вьюмодель; (в) физган (луч → проп → таскание); +(г) скрытие своей модели, когда камера внутри неё; (д) скелет+анимации. ## 3. Что уже сделано и лежит в `main`