Rotate camera with mouse
ci/woodpecker/push/build Pipeline was successful
Details
ci/woodpecker/push/build Pipeline was successful
Details
This commit is contained in:
parent
249b87afed
commit
f3c25b6370
|
@ -1,14 +1,21 @@
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use edict::{World, Component};
|
use edict::{Component, World};
|
||||||
use lyra_engine::{math::{Vec3, Angle, Quat}, input::{InputButtons, KeyCode, MouseMotion}, ecs::{SimpleSystem, components::camera::CameraComponent, EventQueue}, game::Game, plugin::Plugin};
|
use lyra_engine::{
|
||||||
|
ecs::{components::camera::CameraComponent, EventQueue, SimpleSystem},
|
||||||
|
game::Game,
|
||||||
|
input::{InputButtons, KeyCode, MouseMotion},
|
||||||
|
math::{Angle, Quat, Vec3},
|
||||||
|
plugin::Plugin,
|
||||||
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Clone, Component)]
|
#[derive(Clone, Component)]
|
||||||
pub struct FreeFlyCamera {
|
pub struct FreeFlyCamera {
|
||||||
pub speed: f32,
|
pub speed: f32,
|
||||||
pub look_speed: f32,
|
pub look_speed: f32,
|
||||||
pub look_with_keys: bool
|
pub mouse_sensitivity: f32,
|
||||||
|
pub look_with_keys: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FreeFlyCamera {
|
impl Default for FreeFlyCamera {
|
||||||
|
@ -16,17 +23,19 @@ impl Default for FreeFlyCamera {
|
||||||
Self {
|
Self {
|
||||||
speed: 0.07,
|
speed: 0.07,
|
||||||
look_speed: 0.01,
|
look_speed: 0.01,
|
||||||
look_with_keys: false
|
mouse_sensitivity: 0.03,
|
||||||
|
look_with_keys: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FreeFlyCamera {
|
impl FreeFlyCamera {
|
||||||
pub fn new(speed: f32, look_speed: f32, look_with_keys: bool) -> Self {
|
pub fn new(speed: f32, look_speed: f32, mouse_sensitivity: f32, look_with_keys: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
speed,
|
speed,
|
||||||
look_speed,
|
look_speed,
|
||||||
look_with_keys
|
mouse_sensitivity,
|
||||||
|
look_with_keys,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,77 +46,93 @@ impl SimpleSystem for FreeFlyCameraController {
|
||||||
fn execute_mut(&mut self, world: &mut World) -> anyhow::Result<()> {
|
fn execute_mut(&mut self, world: &mut World) -> anyhow::Result<()> {
|
||||||
let mut camera_rot = Vec3::default();
|
let mut camera_rot = Vec3::default();
|
||||||
|
|
||||||
let keys = world.get_resource::<InputButtons<KeyCode>>()
|
let events = world
|
||||||
|
.get_resource_mut::<EventQueue>()
|
||||||
|
.and_then(|q| q.read_events::<MouseMotion>());
|
||||||
|
|
||||||
|
let keys = world
|
||||||
|
.get_resource::<InputButtons<KeyCode>>()
|
||||||
.map(|r| r.deref().clone());
|
.map(|r| r.deref().clone());
|
||||||
if keys.is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let keys = keys.unwrap();
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::Left) {
|
if let Some(keys) = keys.as_ref() {
|
||||||
camera_rot.y += 1.0;
|
if keys.is_pressed(KeyCode::Left) {
|
||||||
|
camera_rot.y += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::Right) {
|
||||||
|
camera_rot.y -= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::Up) {
|
||||||
|
camera_rot.x += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::Down) {
|
||||||
|
camera_rot.x -= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::E) {
|
||||||
|
camera_rot.z -= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::Q) {
|
||||||
|
camera_rot.z += 1.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::Right) {
|
for (cam, fly) in world
|
||||||
camera_rot.y -= 1.0;
|
.query_mut::<(&mut CameraComponent, &mut FreeFlyCamera)>()
|
||||||
}
|
.iter_mut()
|
||||||
|
{
|
||||||
if keys.is_pressed(KeyCode::Up) {
|
|
||||||
camera_rot.x += 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::Down) {
|
|
||||||
camera_rot.x -= 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::E) {
|
|
||||||
camera_rot.z -= 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::Q) {
|
|
||||||
camera_rot.z += 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let camera_rot = camera_rot.normalize();
|
|
||||||
|
|
||||||
for (cam, fly) in world.query_mut::<(&mut CameraComponent, &mut FreeFlyCamera)>().iter_mut() {
|
|
||||||
let forward = cam.transform.forward();
|
let forward = cam.transform.forward();
|
||||||
let left = cam.transform.left();
|
let left = cam.transform.left();
|
||||||
let up = cam.transform.up();
|
let up = cam.transform.up();
|
||||||
|
|
||||||
let mut velocity = Vec3::ZERO;
|
// handle camera movement
|
||||||
|
if let Some(keys) = keys.as_ref() {
|
||||||
|
let mut velocity = Vec3::ZERO;
|
||||||
|
if keys.is_pressed(KeyCode::A) {
|
||||||
|
velocity -= left;
|
||||||
|
}
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::A) {
|
if keys.is_pressed(KeyCode::D) {
|
||||||
velocity -= left;
|
velocity += left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::W) {
|
||||||
|
velocity += forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::S) {
|
||||||
|
velocity -= forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::C) {
|
||||||
|
velocity += up;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keys.is_pressed(KeyCode::Z) {
|
||||||
|
velocity -= up;
|
||||||
|
}
|
||||||
|
|
||||||
|
if velocity != Vec3::ZERO {
|
||||||
|
cam.transform.translation += velocity.normalize() * fly.speed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::D) {
|
// handle camera rotation
|
||||||
velocity += left;
|
if let Some(mut events) = events.clone() {
|
||||||
|
while let Some(motion) = events.pop_front() {
|
||||||
|
camera_rot.x -= motion.delta.y * fly.mouse_sensitivity;
|
||||||
|
camera_rot.y -= motion.delta.x * fly.mouse_sensitivity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::W) {
|
if camera_rot != Vec3::ZERO {
|
||||||
velocity += forward;
|
let look_velocity = camera_rot * fly.look_speed;
|
||||||
}
|
cam.transform.rotation *= Quat::from_rotation_x(look_velocity.x)
|
||||||
|
* Quat::from_rotation_y(look_velocity.y)
|
||||||
if keys.is_pressed(KeyCode::S) {
|
* Quat::from_rotation_z(look_velocity.z);
|
||||||
velocity -= forward;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::C) {
|
|
||||||
velocity += up;
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_pressed(KeyCode::Z) {
|
|
||||||
velocity -= up;
|
|
||||||
}
|
|
||||||
|
|
||||||
if velocity != Vec3::ZERO {
|
|
||||||
cam.transform.translation += velocity.normalize() * fly.speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !camera_rot.is_nan() {
|
|
||||||
let look_velocity = camera_rot.normalize() * fly.look_speed;
|
|
||||||
cam.transform.rotation *= Quat::from_rotation_x(look_velocity.x) * Quat::from_rotation_y(look_velocity.y) * Quat::from_rotation_z(look_velocity.z);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,11 @@ pub const INDICES: &[u16] = &[
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let setup_sys = |world: &mut World| -> anyhow::Result<()> {
|
let setup_sys = |world: &mut World| -> anyhow::Result<()> {
|
||||||
/* {
|
{
|
||||||
let mut window_options = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
|
let mut window_options = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
|
||||||
window_options.cursor_grab = CursorGrabMode::Confined;
|
window_options.cursor_grab = CursorGrabMode::Confined;
|
||||||
window_options.cursor_visible = false;
|
window_options.cursor_visible = false;
|
||||||
} */
|
}
|
||||||
|
|
||||||
let mut resman = world.get_resource_mut::<ResourceManager>().unwrap();
|
let mut resman = world.get_resource_mut::<ResourceManager>().unwrap();
|
||||||
//let diffuse_texture = resman.request::<Texture>("assets/happy-tree.png").unwrap();
|
//let diffuse_texture = resman.request::<Texture>("assets/happy-tree.png").unwrap();
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
use edict::Component;
|
||||||
|
|
||||||
|
use crate::{math::{Angle, Transform}, render::camera::CameraProjectionMode};
|
||||||
|
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
pub struct FreeFlyCamera {
|
||||||
|
pub transform: Transform,
|
||||||
|
pub fov: Angle,
|
||||||
|
pub mode: CameraProjectionMode,
|
||||||
|
pub speed: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FreeFlyCamera {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FreeFlyCamera {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
transform: Transform::default(),
|
||||||
|
fov: Angle::Degrees(45.0),
|
||||||
|
mode: CameraProjectionMode::Perspective,
|
||||||
|
speed: 1.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,3 +2,4 @@ pub mod mesh;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
|
pub mod free_fly_camera;
|
31
src/game.rs
31
src/game.rs
|
@ -2,7 +2,7 @@ use std::{sync::Arc, collections::VecDeque};
|
||||||
|
|
||||||
use async_std::task::block_on;
|
use async_std::task::block_on;
|
||||||
|
|
||||||
use tracing::{info, error, Level};
|
use tracing::{info, error, Level, debug};
|
||||||
use tracing_appender::non_blocking;
|
use tracing_appender::non_blocking;
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
layer::SubscriberExt,
|
layer::SubscriberExt,
|
||||||
|
@ -12,7 +12,7 @@ use tracing_subscriber::{
|
||||||
|
|
||||||
use winit::{window::{WindowBuilder, Window}, event::{Event, WindowEvent, KeyboardInput, ElementState, VirtualKeyCode, DeviceEvent}, event_loop::{EventLoop, ControlFlow}};
|
use winit::{window::{WindowBuilder, Window}, event::{Event, WindowEvent, KeyboardInput, ElementState, VirtualKeyCode, DeviceEvent}, event_loop::{EventLoop, ControlFlow}};
|
||||||
|
|
||||||
use crate::{render::renderer::{Renderer, BasicRenderer}, input_event::InputEvent, ecs::{SimpleSystem, SystemDispatcher, EventQueue, Events}, plugin::Plugin};
|
use crate::{render::{renderer::{Renderer, BasicRenderer}, window::WindowOptions}, input_event::InputEvent, ecs::{SimpleSystem, SystemDispatcher, EventQueue, Events}, plugin::Plugin, change_tracker::Ct};
|
||||||
|
|
||||||
pub struct Controls<'a> {
|
pub struct Controls<'a> {
|
||||||
pub world: &'a mut edict::World,
|
pub world: &'a mut edict::World,
|
||||||
|
@ -91,21 +91,6 @@ impl GameLoop {
|
||||||
Some(ControlFlow::Exit)
|
Some(ControlFlow::Exit)
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Create system for this? or maybe merge into input system, idk
|
|
||||||
InputEvent::CursorEntered { .. } => {
|
|
||||||
let state = self.world.with_resource(WindowState::new);
|
|
||||||
state.is_cursor_inside_window = true;
|
|
||||||
|
|
||||||
None
|
|
||||||
},
|
|
||||||
|
|
||||||
InputEvent::CursorLeft { .. } => {
|
|
||||||
let state = self.world.with_resource(WindowState::new);
|
|
||||||
state.is_cursor_inside_window = false;
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
//debug!("Got unhandled input event: \"{:?}\"", event);
|
//debug!("Got unhandled input event: \"{:?}\"", event);
|
||||||
|
|
||||||
|
@ -122,16 +107,20 @@ impl GameLoop {
|
||||||
*control_flow = ControlFlow::Poll;
|
*control_flow = ControlFlow::Poll;
|
||||||
match event {
|
match event {
|
||||||
Event::DeviceEvent { device_id, event: DeviceEvent::MouseMotion { delta } } => {
|
Event::DeviceEvent { device_id, event: DeviceEvent::MouseMotion { delta } } => {
|
||||||
|
//debug!("motion: {delta:?}");
|
||||||
// convert a MouseMotion event to an InputEvent
|
// convert a MouseMotion event to an InputEvent
|
||||||
// make sure that the mouse is inside the window and the mouse has focus before reporting mouse motion
|
// make sure that the mouse is inside the window and the mouse has focus before reporting mouse motion
|
||||||
let trigger = matches!(self.world.get_resource::<WindowState>(), Some(window_state)
|
/* let trigger = matches!(self.world.get_resource::<WindowState>(), Some(window_state)
|
||||||
if window_state.is_focused && window_state.is_cursor_inside_window);
|
if window_state.is_focused && window_state.is_cursor_inside_window); */
|
||||||
|
|
||||||
|
let trigger = matches!(self.world.get_resource::<Ct<WindowOptions>>(), Some(window)
|
||||||
|
if window.focused && window.cursor_inside_window);
|
||||||
|
|
||||||
if trigger {
|
if trigger {
|
||||||
let event_queue = self.world.with_resource(Events::<InputEvent>::new);
|
let mut event_queue = self.world.get_resource_mut::<EventQueue>().unwrap();
|
||||||
|
|
||||||
let input_event = InputEvent::MouseMotion { device_id, delta, };
|
let input_event = InputEvent::MouseMotion { device_id, delta, };
|
||||||
event_queue.push_back(input_event);
|
event_queue.trigger_event(input_event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Event::WindowEvent {
|
Event::WindowEvent {
|
||||||
|
|
|
@ -258,8 +258,6 @@ impl InputSystem {
|
||||||
delta: Vec2::new(delta.0 as f32, delta.1 as f32)
|
delta: Vec2::new(delta.0 as f32, delta.1 as f32)
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("delta: {:?}", delta);
|
|
||||||
|
|
||||||
event_queue.trigger_event(delta);
|
event_queue.trigger_event(delta);
|
||||||
},
|
},
|
||||||
InputEvent::CursorMoved { position, .. } => {
|
InputEvent::CursorMoved { position, .. } => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::{sync::Arc, collections::VecDeque};
|
||||||
|
|
||||||
use glam::{Vec2, IVec2};
|
use glam::{Vec2, IVec2};
|
||||||
use tracing::{warn, error};
|
use tracing::{warn, error};
|
||||||
|
@ -6,7 +6,7 @@ use winit::{window::{Window, Fullscreen}, dpi::{LogicalPosition, LogicalSize, Ph
|
||||||
|
|
||||||
pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel};
|
pub use winit::window::{CursorGrabMode, CursorIcon, Icon, Theme, WindowButtons, WindowLevel};
|
||||||
|
|
||||||
use crate::{plugin::Plugin, change_tracker::Ct};
|
use crate::{plugin::Plugin, change_tracker::Ct, ecs::EventQueue, input_event::InputEvent};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub enum WindowMode {
|
pub enum WindowMode {
|
||||||
|
@ -171,6 +171,12 @@ pub struct WindowOptions {
|
||||||
/// Change the window level.
|
/// Change the window level.
|
||||||
/// This is just a hint to the OS, and the system could ignore it.
|
/// This is just a hint to the OS, and the system could ignore it.
|
||||||
pub level: WindowLevel,
|
pub level: WindowLevel,
|
||||||
|
|
||||||
|
/// Get/set the window's focused state.
|
||||||
|
pub focused: bool,
|
||||||
|
|
||||||
|
/// Get whether or not the cursor is inside the window.
|
||||||
|
pub cursor_inside_window: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WindowOptions {
|
impl Default for WindowOptions {
|
||||||
|
@ -198,6 +204,8 @@ impl Default for WindowOptions {
|
||||||
title: "Lyra Engine Game".to_string(),
|
title: "Lyra Engine Game".to_string(),
|
||||||
icon: None,
|
icon: None,
|
||||||
level: WindowLevel::Normal,
|
level: WindowLevel::Normal,
|
||||||
|
focused: false,
|
||||||
|
cursor_inside_window: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,10 +260,11 @@ fn set_cursor_grab(window: &Window, grab: &mut CursorGrabMode) -> anyhow::Result
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// if the window is set to confine the cursor, and the cursor is invisible,
|
/// if the window is set to confine the cursor, the cursor is invisible,
|
||||||
/// set the cursor position to the center of the screen.
|
/// and the window is focused, set the cursor position to the center of the screen.
|
||||||
fn center_mouse(window: &Window, options: &WindowOptions) {
|
fn center_mouse(window: &Window, options: &WindowOptions) {
|
||||||
if options.cursor_grab == CursorGrabMode::Confined && !options.cursor_visible {
|
if options.cursor_grab == CursorGrabMode::Confined && !options.cursor_visible
|
||||||
|
&& options.focused {
|
||||||
let size = window.inner_size();
|
let size = window.inner_size();
|
||||||
let middle = PhysicalPosition {
|
let middle = PhysicalPosition {
|
||||||
x: size.width / 2,
|
x: size.width / 2,
|
||||||
|
@ -269,11 +278,14 @@ fn window_updater_system(world: &mut edict::World) -> anyhow::Result<()> {
|
||||||
if let (Some(window), Some(opts)) = (world.get_resource::<Arc<Window>>(), world.get_resource::<Ct<WindowOptions>>()) {
|
if let (Some(window), Some(opts)) = (world.get_resource::<Arc<Window>>(), world.get_resource::<Ct<WindowOptions>>()) {
|
||||||
// if the options changed, update the window
|
// if the options changed, update the window
|
||||||
if opts.peek_changed() {
|
if opts.peek_changed() {
|
||||||
drop(opts); // avoid attempting to get a RefMut when we already have a Ref out.
|
drop(opts); // drop the Ref, we're about to get a RefMut
|
||||||
|
|
||||||
// now we can get it mutable, this will trigger the ChangeTracker, so it will be reset at the end of this scope.
|
// now we can get it mutable, this will trigger the ChangeTracker, so it will be reset at the end of this scope.
|
||||||
let mut opts = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
|
let mut opts = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
|
||||||
|
|
||||||
|
if opts.focused {
|
||||||
|
window.focus_window();
|
||||||
|
}
|
||||||
window.set_content_protected(opts.content_protected);
|
window.set_content_protected(opts.content_protected);
|
||||||
set_cursor_grab(&window, &mut opts.cursor_grab)?;
|
set_cursor_grab(&window, &mut opts.cursor_grab)?;
|
||||||
match window.set_cursor_hittest(opts.cursor_hittest) {
|
match window.set_cursor_hittest(opts.cursor_hittest) {
|
||||||
|
@ -324,6 +336,31 @@ fn window_updater_system(world: &mut edict::World) -> anyhow::Result<()> {
|
||||||
|
|
||||||
center_mouse(&window, &opts);
|
center_mouse(&window, &opts);
|
||||||
} else {
|
} else {
|
||||||
|
drop(opts); // drop the Ref, we're about to get a RefMut
|
||||||
|
let mut opts = world.get_resource_mut::<Ct<WindowOptions>>().unwrap();
|
||||||
|
|
||||||
|
if let Some(event_queue) = world.get_resource_mut::<EventQueue>() {
|
||||||
|
if let Some(mut events) = event_queue.read_events::<InputEvent>() {
|
||||||
|
while let Some(ev) = events.pop_front() {
|
||||||
|
match ev {
|
||||||
|
InputEvent::CursorEntered { .. } => {
|
||||||
|
opts.cursor_inside_window = true;
|
||||||
|
},
|
||||||
|
InputEvent::CursorLeft { .. } => {
|
||||||
|
opts.cursor_inside_window = false;
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the stored state of the window to match the actual window
|
||||||
|
|
||||||
|
opts.focused = window.has_focus();
|
||||||
|
|
||||||
|
opts.reset();
|
||||||
|
|
||||||
center_mouse(&window, &opts);
|
center_mouse(&window, &opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue