Constant cleanup
parent
2fda557959
commit
02220902ca
|
@ -580,6 +580,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cgmath",
|
"cgmath",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
"galactica-constants",
|
||||||
"galactica-content",
|
"galactica-content",
|
||||||
"galactica-render",
|
"galactica-render",
|
||||||
"image",
|
"image",
|
||||||
|
@ -592,6 +593,10 @@ dependencies = [
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "galactica-constants"
|
||||||
|
version = "0.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "galactica-content"
|
name = "galactica-content"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
@ -612,6 +617,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cgmath",
|
"cgmath",
|
||||||
|
"galactica-constants",
|
||||||
"galactica-content",
|
"galactica-content",
|
||||||
"image",
|
"image",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
@ -28,13 +28,14 @@ rpath = false
|
||||||
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/content", "crates/render"]
|
members = ["crates/content", "crates/render", "crates/constants"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Internal crates
|
# Internal crates
|
||||||
galactica-content = { path = "crates/content" }
|
galactica-content = { path = "crates/content" }
|
||||||
galactica-render = { path = "crates/render" }
|
galactica-render = { path = "crates/render" }
|
||||||
|
galactica-constants = { path = "crates/constants" }
|
||||||
|
|
||||||
# Files
|
# Files
|
||||||
image = { version = "0.24", features = ["png"] }
|
image = { version = "0.24", features = ["png"] }
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
[package]
|
||||||
|
name = "galactica-constants"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
|
@ -24,3 +24,19 @@ pub const STARFIELD_DENSITY: f64 = 0.01;
|
||||||
/// Number of stars in one starfield tile
|
/// Number of stars in one starfield tile
|
||||||
/// Must fit inside an i32
|
/// Must fit inside an i32
|
||||||
pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64;
|
pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64;
|
||||||
|
|
||||||
|
/// Name of starfield texture
|
||||||
|
pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield";
|
||||||
|
|
||||||
|
/// Root directory of game content
|
||||||
|
pub const CONTENT_ROOT: &'static str = "./content";
|
||||||
|
|
||||||
|
/// Root directory of game textures
|
||||||
|
pub const TEXTURE_ROOT: &'static str = "./assets/render";
|
||||||
|
|
||||||
|
// We can draw at most this many sprites on the screen.
|
||||||
|
// TODO: compile-time option or config file
|
||||||
|
pub const SPRITE_INSTANCE_LIMIT: u64 = 500;
|
||||||
|
|
||||||
|
// Must be small enough to fit in an i32
|
||||||
|
pub const STARFIELD_INSTANCE_LIMIT: u64 = STARFIELD_COUNT * 24;
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Internal crates
|
# Internal crates
|
||||||
galactica-content = { path = "../content" }
|
galactica-content = { path = "../content" }
|
||||||
|
galactica-constants = { path = "../constants" }
|
||||||
|
|
||||||
# Misc helpers
|
# Misc helpers
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
use crate::consts_main;
|
|
||||||
use cgmath::Matrix4;
|
|
||||||
|
|
||||||
// We can draw at most this many sprites on the screen.
|
|
||||||
// TODO: compile-time option or config file
|
|
||||||
pub const SPRITE_INSTANCE_LIMIT: u64 = 500;
|
|
||||||
|
|
||||||
// Must be small enough to fit in an i32
|
|
||||||
pub const STARFIELD_INSTANCE_LIMIT: u64 = consts_main::STARFIELD_COUNT * 24;
|
|
||||||
|
|
||||||
/// Shader entry points
|
|
||||||
pub const SHADER_MAIN_VERTEX: &'static str = "vertex_main";
|
|
||||||
pub const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main";
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
|
|
||||||
1.0, 0.0, 0.0, 0.0,
|
|
||||||
0.0, 1.0, 0.0, 0.0,
|
|
||||||
0.0, 0.0, 0.5, 0.5,
|
|
||||||
0.0, 0.0, 0.0, 1.0,
|
|
||||||
);
|
|
|
@ -1,13 +1,13 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bytemuck;
|
use bytemuck;
|
||||||
use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3};
|
use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3};
|
||||||
|
use galactica_constants;
|
||||||
use std::{iter, rc::Rc};
|
use std::{iter, rc::Rc};
|
||||||
use wgpu;
|
use wgpu;
|
||||||
use winit::{self, dpi::LogicalSize, window::Window};
|
use winit::{self, dpi::LogicalSize, window::Window};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::{OPENGL_TO_WGPU_MATRIX, SPRITE_INSTANCE_LIMIT, STARFIELD_INSTANCE_LIMIT},
|
content,
|
||||||
consts_main, content,
|
|
||||||
globaldata::{GlobalData, GlobalDataContent},
|
globaldata::{GlobalData, GlobalDataContent},
|
||||||
pipeline::PipelineBuilder,
|
pipeline::PipelineBuilder,
|
||||||
sprite::ObjectSubSprite,
|
sprite::ObjectSubSprite,
|
||||||
|
@ -18,7 +18,7 @@ use crate::{
|
||||||
types::{SpriteInstance, StarfieldInstance, TexturedVertex},
|
types::{SpriteInstance, StarfieldInstance, TexturedVertex},
|
||||||
VertexBuffer,
|
VertexBuffer,
|
||||||
},
|
},
|
||||||
ObjectSprite, UiSprite,
|
ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A high-level GPU wrapper. Consumes game state,
|
/// A high-level GPU wrapper. Consumes game state,
|
||||||
|
@ -122,7 +122,7 @@ impl GPUState {
|
||||||
&device,
|
&device,
|
||||||
Some(SPRITE_VERTICES),
|
Some(SPRITE_VERTICES),
|
||||||
Some(SPRITE_INDICES),
|
Some(SPRITE_INDICES),
|
||||||
SPRITE_INSTANCE_LIMIT,
|
galactica_constants::SPRITE_INSTANCE_LIMIT,
|
||||||
)),
|
)),
|
||||||
|
|
||||||
starfield: Rc::new(VertexBuffer::new::<TexturedVertex, StarfieldInstance>(
|
starfield: Rc::new(VertexBuffer::new::<TexturedVertex, StarfieldInstance>(
|
||||||
|
@ -130,7 +130,7 @@ impl GPUState {
|
||||||
&device,
|
&device,
|
||||||
Some(SPRITE_VERTICES),
|
Some(SPRITE_VERTICES),
|
||||||
Some(SPRITE_INDICES),
|
Some(SPRITE_INDICES),
|
||||||
STARFIELD_INSTANCE_LIMIT,
|
galactica_constants::STARFIELD_INSTANCE_LIMIT,
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -409,7 +409,7 @@ impl GPUState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce sprite limit
|
// Enforce sprite limit
|
||||||
if instances.len() as u64 > SPRITE_INSTANCE_LIMIT {
|
if instances.len() as u64 > galactica_constants::SPRITE_INSTANCE_LIMIT {
|
||||||
// TODO: no panic, handle this better.
|
// TODO: no panic, handle this better.
|
||||||
panic!("Sprite limit exceeded!")
|
panic!("Sprite limit exceeded!")
|
||||||
}
|
}
|
||||||
|
@ -476,17 +476,17 @@ impl GPUState {
|
||||||
bytemuck::cast_slice(&[GlobalDataContent {
|
bytemuck::cast_slice(&[GlobalDataContent {
|
||||||
camera_position: camera_pos.into(),
|
camera_position: camera_pos.into(),
|
||||||
camera_zoom: [camera_zoom, 0.0],
|
camera_zoom: [camera_zoom, 0.0],
|
||||||
camera_zoom_limits: [consts_main::ZOOM_MIN, consts_main::ZOOM_MAX],
|
camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX],
|
||||||
window_size: [
|
window_size: [
|
||||||
self.window_size.width as f32,
|
self.window_size.width as f32,
|
||||||
self.window_size.height as f32,
|
self.window_size.height as f32,
|
||||||
],
|
],
|
||||||
window_aspect: [self.window_aspect, 0.0],
|
window_aspect: [self.window_aspect, 0.0],
|
||||||
starfield_texture: [self.texture_array.get_starfield_texture().index, 0],
|
starfield_texture: [self.texture_array.get_starfield_texture().index, 0],
|
||||||
starfield_tile_size: [consts_main::STARFIELD_SIZE as f32, 0.0],
|
starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0],
|
||||||
starfield_size_limits: [
|
starfield_size_limits: [
|
||||||
consts_main::STARFIELD_SIZE_MIN,
|
galactica_constants::STARFIELD_SIZE_MIN,
|
||||||
consts_main::STARFIELD_SIZE_MAX,
|
galactica_constants::STARFIELD_SIZE_MAX,
|
||||||
],
|
],
|
||||||
}]),
|
}]),
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//! and the only one external code should interact with.
|
//! and the only one external code should interact with.
|
||||||
//! (Excluding data structs, like [`ObjectSprite`])
|
//! (Excluding data structs, like [`ObjectSprite`])
|
||||||
|
|
||||||
mod consts;
|
|
||||||
mod globaldata;
|
mod globaldata;
|
||||||
mod gpustate;
|
mod gpustate;
|
||||||
mod pipeline;
|
mod pipeline;
|
||||||
|
@ -16,9 +15,20 @@ mod starfield;
|
||||||
mod texturearray;
|
mod texturearray;
|
||||||
mod vertexbuffer;
|
mod vertexbuffer;
|
||||||
|
|
||||||
// TODO: remove
|
|
||||||
mod consts_main;
|
|
||||||
|
|
||||||
use galactica_content as content;
|
use galactica_content as content;
|
||||||
pub use gpustate::GPUState;
|
pub use gpustate::GPUState;
|
||||||
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite};
|
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite};
|
||||||
|
|
||||||
|
use cgmath::Matrix4;
|
||||||
|
|
||||||
|
/// Shader entry points
|
||||||
|
pub(crate) const SHADER_MAIN_VERTEX: &'static str = "vertex_main";
|
||||||
|
pub(crate) const SHADER_MAIN_FRAGMENT: &'static str = "fragment_main";
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub(crate) const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
|
||||||
|
1.0, 0.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 0.5, 0.5,
|
||||||
|
0.0, 0.0, 0.0, 1.0,
|
||||||
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use wgpu;
|
use wgpu;
|
||||||
|
|
||||||
use crate::consts::{SHADER_MAIN_FRAGMENT, SHADER_MAIN_VERTEX};
|
|
||||||
use crate::vertexbuffer::VertexBuffer;
|
use crate::vertexbuffer::VertexBuffer;
|
||||||
|
use crate::{SHADER_MAIN_FRAGMENT, SHADER_MAIN_VERTEX};
|
||||||
|
|
||||||
pub struct PipelineBuilder<'a> {
|
pub struct PipelineBuilder<'a> {
|
||||||
// These are provided with new()
|
// These are provided with new()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use cgmath::{Point2, Point3, Vector2, Vector3};
|
use cgmath::{Point2, Point3, Vector2, Vector3};
|
||||||
|
use galactica_constants;
|
||||||
use rand::{self, Rng};
|
use rand::{self, Rng};
|
||||||
|
|
||||||
use crate::{consts, consts_main, vertexbuffer::types::StarfieldInstance};
|
use crate::vertexbuffer::types::StarfieldInstance;
|
||||||
|
|
||||||
pub(crate) struct StarfieldStar {
|
pub(crate) struct StarfieldStar {
|
||||||
/// Star coordinates, in world space.
|
/// Star coordinates, in world space.
|
||||||
|
@ -33,16 +34,20 @@ impl Starfield {
|
||||||
pub fn regenerate(&mut self) {
|
pub fn regenerate(&mut self) {
|
||||||
// TODO: save seed in system, regenerate on jump
|
// TODO: save seed in system, regenerate on jump
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let sz = consts_main::STARFIELD_SIZE as f32 / 2.0;
|
let sz = galactica_constants::STARFIELD_SIZE as f32 / 2.0;
|
||||||
self.stars = (0..consts_main::STARFIELD_COUNT)
|
self.stars = (0..galactica_constants::STARFIELD_COUNT)
|
||||||
.map(|_| StarfieldStar {
|
.map(|_| StarfieldStar {
|
||||||
pos: Point3 {
|
pos: Point3 {
|
||||||
x: rng.gen_range(-sz..=sz),
|
x: rng.gen_range(-sz..=sz),
|
||||||
y: rng.gen_range(-sz..=sz),
|
y: rng.gen_range(-sz..=sz),
|
||||||
z: rng.gen_range(consts_main::STARFIELD_Z_MIN..consts_main::STARFIELD_Z_MAX),
|
z: rng.gen_range(
|
||||||
|
galactica_constants::STARFIELD_Z_MIN..galactica_constants::STARFIELD_Z_MAX,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
size: rng
|
size: rng.gen_range(
|
||||||
.gen_range(consts_main::STARFIELD_SIZE_MIN..consts_main::STARFIELD_SIZE_MAX),
|
galactica_constants::STARFIELD_SIZE_MIN
|
||||||
|
..galactica_constants::STARFIELD_SIZE_MAX,
|
||||||
|
),
|
||||||
tint: Vector2 {
|
tint: Vector2 {
|
||||||
x: rng.gen_range(0.0..=1.0),
|
x: rng.gen_range(0.0..=1.0),
|
||||||
y: rng.gen_range(0.0..=1.0),
|
y: rng.gen_range(0.0..=1.0),
|
||||||
|
@ -52,17 +57,17 @@ impl Starfield {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_instances(&mut self, aspect: f32) -> Vec<StarfieldInstance> {
|
pub fn make_instances(&mut self, aspect: f32) -> Vec<StarfieldInstance> {
|
||||||
let sz = consts_main::STARFIELD_SIZE as f32;
|
let sz = galactica_constants::STARFIELD_SIZE as f32;
|
||||||
|
|
||||||
// Compute window size in starfield tiles
|
// Compute window size in starfield tiles
|
||||||
let mut nw_tile: Point2<i32> = {
|
let mut nw_tile: Point2<i32> = {
|
||||||
// Game coordinates (relative to camera) of nw corner of screen.
|
// Game coordinates (relative to camera) of nw corner of screen.
|
||||||
let clip_nw = Point2::from((aspect, 1.0)) * consts_main::ZOOM_MAX;
|
let clip_nw = Point2::from((aspect, 1.0)) * galactica_constants::ZOOM_MAX;
|
||||||
|
|
||||||
// Parallax correction.
|
// Parallax correction.
|
||||||
// Also, adjust v for mod to work properly
|
// Also, adjust v for mod to work properly
|
||||||
// (v is centered at 0)
|
// (v is centered at 0)
|
||||||
let v: Point2<f32> = clip_nw * consts_main::STARFIELD_Z_MIN;
|
let v: Point2<f32> = clip_nw * galactica_constants::STARFIELD_Z_MIN;
|
||||||
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
|
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
@ -86,8 +91,10 @@ impl Starfield {
|
||||||
|
|
||||||
// Truncate tile grid to buffer size
|
// Truncate tile grid to buffer size
|
||||||
// (The window won't be full of stars if our instance limit is too small)
|
// (The window won't be full of stars if our instance limit is too small)
|
||||||
while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * consts_main::STARFIELD_COUNT as i32)
|
while ((nw_tile.x * 2 + 1)
|
||||||
> consts::STARFIELD_INSTANCE_LIMIT as i32
|
* (nw_tile.y * 2 + 1)
|
||||||
|
* galactica_constants::STARFIELD_COUNT as i32)
|
||||||
|
> galactica_constants::STARFIELD_INSTANCE_LIMIT as i32
|
||||||
{
|
{
|
||||||
nw_tile -= Vector2::from((1, 1));
|
nw_tile -= Vector2::from((1, 1));
|
||||||
}
|
}
|
||||||
|
@ -112,7 +119,7 @@ impl Starfield {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce starfield limit
|
// Enforce starfield limit
|
||||||
if instances.len() as u64 > consts::STARFIELD_INSTANCE_LIMIT {
|
if instances.len() as u64 > galactica_constants::STARFIELD_INSTANCE_LIMIT {
|
||||||
unreachable!("Starfield limit exceeded!")
|
unreachable!("Starfield limit exceeded!")
|
||||||
}
|
}
|
||||||
self.instance_count = instances.len() as u32;
|
self.instance_count = instances.len() as u32;
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
pub const ZOOM_MIN: f32 = 200.0;
|
|
||||||
pub const ZOOM_MAX: f32 = 2000.0;
|
|
||||||
|
|
||||||
/// Name of starfield texture
|
|
||||||
pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield";
|
|
||||||
|
|
||||||
/// Root directory of game content
|
|
||||||
pub const CONTENT_ROOT: &'static str = "./content";
|
|
||||||
|
|
||||||
/// Root directory of game textures
|
|
||||||
pub const TEXTURE_ROOT: &'static str = "./assets/render";
|
|
|
@ -1,11 +1,12 @@
|
||||||
use cgmath::{Deg, InnerSpace, Point2};
|
use cgmath::{Deg, InnerSpace, Point2};
|
||||||
|
use galactica_constants;
|
||||||
use galactica_render::{AnchoredUiPosition, ObjectSprite, UiSprite};
|
use galactica_render::{AnchoredUiPosition, ObjectSprite, UiSprite};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
|
use winit::event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
|
||||||
|
|
||||||
use super::{camera::Camera, outfits, system::System};
|
use super::{camera::Camera, outfits, system::System};
|
||||||
use crate::{
|
use crate::{
|
||||||
consts, content,
|
content,
|
||||||
inputstatus::InputStatus,
|
inputstatus::InputStatus,
|
||||||
physics::{util, Physics, ShipHandle},
|
physics::{util, Physics, ShipHandle},
|
||||||
shipbehavior::{self, ShipBehavior},
|
shipbehavior::{self, ShipBehavior},
|
||||||
|
@ -179,8 +180,8 @@ impl Game {
|
||||||
self.physics.step(t, &self.content);
|
self.physics.step(t, &self.content);
|
||||||
|
|
||||||
if self.input.v_scroll != 0.0 {
|
if self.input.v_scroll != 0.0 {
|
||||||
self.camera.zoom =
|
self.camera.zoom = (self.camera.zoom + self.input.v_scroll)
|
||||||
(self.camera.zoom + self.input.v_scroll).clamp(consts::ZOOM_MIN, consts::ZOOM_MAX);
|
.clamp(galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX);
|
||||||
self.input.v_scroll = 0.0;
|
self.input.v_scroll = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod consts;
|
|
||||||
mod game;
|
mod game;
|
||||||
mod inputstatus;
|
mod inputstatus;
|
||||||
mod objects;
|
mod objects;
|
||||||
|
@ -8,6 +7,7 @@ mod shipbehavior;
|
||||||
pub use galactica_content as content;
|
pub use galactica_content as content;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use galactica_constants;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use winit::{
|
use winit::{
|
||||||
event::{Event, KeyboardInput, WindowEvent},
|
event::{Event, KeyboardInput, WindowEvent},
|
||||||
|
@ -18,9 +18,9 @@ use winit::{
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// TODO: error if missing
|
// TODO: error if missing
|
||||||
let content = content::Content::load_dir(
|
let content = content::Content::load_dir(
|
||||||
PathBuf::from(consts::CONTENT_ROOT),
|
PathBuf::from(galactica_constants::CONTENT_ROOT),
|
||||||
PathBuf::from(consts::TEXTURE_ROOT),
|
PathBuf::from(galactica_constants::TEXTURE_ROOT),
|
||||||
consts::STARFIELD_TEXTURE_NAME.to_owned(),
|
galactica_constants::STARFIELD_TEXTURE_NAME.to_owned(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let event_loop = EventLoop::new();
|
let event_loop = EventLoop::new();
|
||||||
|
|
Loading…
Reference in New Issue