mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-24 00:31:34 +11:00
Merge pull request #40 from corwinkuiper/object-allocation
Object allocation
This commit is contained in:
commit
7595c938c9
|
@ -15,8 +15,8 @@ enum State {
|
||||||
Flapping,
|
Flapping,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Character {
|
struct Character<'a> {
|
||||||
object: ObjectStandard,
|
object: ObjectStandard<'a>,
|
||||||
position: Vector2D,
|
position: Vector2D,
|
||||||
velocity: Vector2D,
|
velocity: Vector2D,
|
||||||
}
|
}
|
||||||
|
@ -151,8 +151,8 @@ fn update_chicken_object(chicken: &mut Character, state: State, frame_count: u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let x: u8 = (chicken.position.x >> 8).try_into().unwrap();
|
let x: u16 = (chicken.position.x >> 8).try_into().unwrap();
|
||||||
let y: u8 = (chicken.position.y >> 8).try_into().unwrap();
|
let y: u16 = (chicken.position.y >> 8).try_into().unwrap();
|
||||||
|
|
||||||
chicken.object.set_x(x - 4);
|
chicken.object.set_x(x - 4);
|
||||||
chicken.object.set_y(y - 4);
|
chicken.object.set_y(y - 4);
|
||||||
|
|
54
agb/src/bitarray.rs
Normal file
54
agb/src/bitarray.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
pub struct Bitarray<const N: usize> {
|
||||||
|
a: [u32; N],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Bitarray<N> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Bitarray { a: [0; N] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, index: usize) -> Option<bool> {
|
||||||
|
if index < N * 32 {
|
||||||
|
Some((self.a[index / 32] >> (index % 32) & 1) != 0)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, index: usize, value: bool) {
|
||||||
|
let value = value as u32;
|
||||||
|
let mask = 1 << (index % 32);
|
||||||
|
let value_mask = value << (index % 32);
|
||||||
|
self.a[index / 32] = self.a[index / 32] & !mask | value_mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case]
|
||||||
|
fn write_and_read(_gba: &mut crate::Gba) {
|
||||||
|
let mut a: Bitarray<2> = Bitarray::new();
|
||||||
|
assert_eq!(a.get(55).unwrap(), false, "expect unset values to be false");
|
||||||
|
a.set(62, true);
|
||||||
|
assert_eq!(a.get(62).unwrap(), true, "expect set value to be true");
|
||||||
|
assert_eq!(a.get(120), None, "expect out of range to give None");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case]
|
||||||
|
fn test_everything(_gba: &mut crate::Gba) {
|
||||||
|
for i in 0..64 {
|
||||||
|
let mut a: Bitarray<2> = Bitarray::new();
|
||||||
|
a.set(i, true);
|
||||||
|
for j in 0..64 {
|
||||||
|
let expected = if i == j { true } else { false };
|
||||||
|
assert_eq!(
|
||||||
|
a.get(j).unwrap(),
|
||||||
|
expected,
|
||||||
|
"set index {} and read {}, expected {} but got {}. u32 of this is {:#b}",
|
||||||
|
i,
|
||||||
|
j,
|
||||||
|
expected,
|
||||||
|
a.get(j).unwrap(),
|
||||||
|
a.a[j / 32],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
|
use core::cell::RefCell;
|
||||||
|
|
||||||
use super::DISPLAY_CONTROL;
|
use super::DISPLAY_CONTROL;
|
||||||
|
use crate::bitarray::Bitarray;
|
||||||
use crate::memory_mapped::MemoryMapped1DArray;
|
use crate::memory_mapped::MemoryMapped1DArray;
|
||||||
|
|
||||||
const OBJECT_ATTRIBUTE_MEMORY: MemoryMapped1DArray<u16, 512> =
|
const OBJECT_ATTRIBUTE_MEMORY: MemoryMapped1DArray<u16, 512> =
|
||||||
|
@ -6,29 +9,39 @@ const OBJECT_ATTRIBUTE_MEMORY: MemoryMapped1DArray<u16, 512> =
|
||||||
|
|
||||||
/// Handles distributing objects and matricies along with operations that effect all objects.
|
/// Handles distributing objects and matricies along with operations that effect all objects.
|
||||||
pub struct ObjectControl {
|
pub struct ObjectControl {
|
||||||
object_count: u8,
|
objects: RefCell<Bitarray<4>>,
|
||||||
affine_count: u8,
|
affines: RefCell<Bitarray<1>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ObjectLoan<'a> {
|
||||||
|
index: u8,
|
||||||
|
objects: &'a RefCell<Bitarray<4>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AffineLoan<'a> {
|
||||||
|
index: u8,
|
||||||
|
affines: &'a RefCell<Bitarray<1>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The standard object, without rotation.
|
/// The standard object, without rotation.
|
||||||
pub struct ObjectStandard {
|
pub struct ObjectStandard<'a> {
|
||||||
attributes: ObjectAttribute,
|
attributes: ObjectAttribute,
|
||||||
id: u8,
|
loan: ObjectLoan<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The affine object, with potential for using a transformation matrix to alter
|
/// The affine object, with potential for using a transformation matrix to alter
|
||||||
/// how the sprite is rendered to screen.
|
/// how the sprite is rendered to screen.
|
||||||
pub struct ObjectAffine {
|
pub struct ObjectAffine<'a> {
|
||||||
attributes: ObjectAttribute,
|
attributes: ObjectAttribute,
|
||||||
id: u8,
|
loan: ObjectLoan<'a>,
|
||||||
aff_id: Option<u8>,
|
aff_id: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refers to an affine matrix in the OAM. Includes both an index and the
|
/// Refers to an affine matrix in the OAM. Includes both an index and the
|
||||||
/// components of the affine matrix.
|
/// components of the affine matrix.
|
||||||
pub struct AffineMatrix {
|
pub struct AffineMatrix<'a> {
|
||||||
pub attributes: AffineMatrixAttributes,
|
pub attributes: AffineMatrixAttributes,
|
||||||
id: u8,
|
loan: AffineLoan<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The components of the affine matrix. The components are fixed point 8:8.
|
/// The components of the affine matrix. The components are fixed point 8:8.
|
||||||
|
@ -66,19 +79,19 @@ pub enum Size {
|
||||||
S32x64 = 0b10_11,
|
S32x64 = 0b10_11,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectStandard {
|
impl ObjectStandard<'_> {
|
||||||
/// Commits the object to OAM such that the updated version is displayed on
|
/// Commits the object to OAM such that the updated version is displayed on
|
||||||
/// screen. Recommend to do this during VBlank.
|
/// screen. Recommend to do this during VBlank.
|
||||||
pub fn commit(&self) {
|
pub fn commit(&self) {
|
||||||
unsafe { self.attributes.commit(self.id) }
|
unsafe { self.attributes.commit(self.loan.index) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the x coordinate of the sprite on screen.
|
/// Sets the x coordinate of the sprite on screen.
|
||||||
pub fn set_x(&mut self, x: u8) {
|
pub fn set_x(&mut self, x: u16) {
|
||||||
self.attributes.set_x(x)
|
self.attributes.set_x(x)
|
||||||
}
|
}
|
||||||
/// Sets the y coordinate of the sprite on screen.
|
/// Sets the y coordinate of the sprite on screen.
|
||||||
pub fn set_y(&mut self, y: u8) {
|
pub fn set_y(&mut self, y: u16) {
|
||||||
self.attributes.set_y(y)
|
self.attributes.set_y(y)
|
||||||
}
|
}
|
||||||
/// Sets the index of the tile to use as the sprite. Potentially a temporary function.
|
/// Sets the index of the tile to use as the sprite. Potentially a temporary function.
|
||||||
|
@ -103,19 +116,19 @@ impl ObjectStandard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectAffine {
|
impl<'a> ObjectAffine<'a> {
|
||||||
/// Commits the object to OAM such that the updated version is displayed on
|
/// Commits the object to OAM such that the updated version is displayed on
|
||||||
/// screen. Recommend to do this during VBlank.
|
/// screen. Recommend to do this during VBlank.
|
||||||
pub fn commit(&self) {
|
pub fn commit(&self) {
|
||||||
unsafe { self.attributes.commit(self.id) }
|
unsafe { self.attributes.commit(self.loan.index) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the x coordinate of the sprite on screen.
|
/// Sets the x coordinate of the sprite on screen.
|
||||||
pub fn set_x(&mut self, x: u8) {
|
pub fn set_x(&mut self, x: u16) {
|
||||||
self.attributes.set_x(x)
|
self.attributes.set_x(x)
|
||||||
}
|
}
|
||||||
/// Sets the y coordinate of the sprite on screen.
|
/// Sets the y coordinate of the sprite on screen.
|
||||||
pub fn set_y(&mut self, y: u8) {
|
pub fn set_y(&mut self, y: u16) {
|
||||||
self.attributes.set_y(y)
|
self.attributes.set_y(y)
|
||||||
}
|
}
|
||||||
/// Sets the index of the tile to use as the sprite. Potentially a temporary function.
|
/// Sets the index of the tile to use as the sprite. Potentially a temporary function.
|
||||||
|
@ -140,9 +153,9 @@ impl ObjectAffine {
|
||||||
}
|
}
|
||||||
/// Sets the affine matrix to use. Changing the affine matrix will change
|
/// Sets the affine matrix to use. Changing the affine matrix will change
|
||||||
/// how the sprite is rendered.
|
/// how the sprite is rendered.
|
||||||
pub fn set_affine_mat(&mut self, aff: &AffineMatrix) {
|
pub fn set_affine_mat(&mut self, aff: &'a AffineMatrix) {
|
||||||
self.attributes.set_affine(aff.id);
|
self.attributes.set_affine(aff.loan.index);
|
||||||
self.aff_id = Some(aff.id);
|
self.aff_id = Some(aff.loan.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +164,20 @@ fn set_bits(current: u16, value: u16, length: u16, shift: u16) -> u16 {
|
||||||
(current & !(mask << shift)) | ((value & mask) << shift)
|
(current & !(mask << shift)) | ((value & mask) << shift)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for ObjectLoan<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut objs = self.objects.borrow_mut();
|
||||||
|
objs.set(self.index as usize, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AffineLoan<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut affs = self.affines.borrow_mut();
|
||||||
|
affs.set(self.index as usize, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ObjectAttribute {
|
struct ObjectAttribute {
|
||||||
a0: u16,
|
a0: u16,
|
||||||
a1: u16,
|
a1: u16,
|
||||||
|
@ -169,19 +196,19 @@ impl ObjectAttribute {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_size(&mut self, size: Size) {
|
fn set_size(&mut self, size: Size) {
|
||||||
let lower = size as u16 & 0b11;
|
let a1 = size as u16 & 0b11;
|
||||||
let upper = (size as u16 >> 2) & 0b11;
|
let a0 = (size as u16 >> 2) & 0b11;
|
||||||
|
|
||||||
self.a0 = set_bits(self.a0, lower, 2, 0xE);
|
self.a0 = set_bits(self.a0, a0, 2, 0xE);
|
||||||
self.a1 = set_bits(self.a1, upper, 2, 0xE);
|
self.a1 = set_bits(self.a1, a1, 2, 0xE);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_x(&mut self, x: u8) {
|
fn set_x(&mut self, x: u16) {
|
||||||
self.a1 = set_bits(self.a1, x as u16, 8, 0);
|
self.a1 = set_bits(self.a1, x, 9, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_y(&mut self, y: u8) {
|
fn set_y(&mut self, y: u16) {
|
||||||
self.a0 = set_bits(self.a0, y as u16, 8, 0)
|
self.a0 = set_bits(self.a0, y, 8, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tile_id(&mut self, id: u16) {
|
fn set_tile_id(&mut self, id: u16) {
|
||||||
|
@ -197,11 +224,11 @@ impl ObjectAttribute {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AffineMatrix {
|
impl AffineMatrix<'_> {
|
||||||
#[allow(clippy::identity_op)]
|
#[allow(clippy::identity_op)]
|
||||||
/// Commits matrix to OAM, will cause any objects using this matrix to be updated.
|
/// Commits matrix to OAM, will cause any objects using this matrix to be updated.
|
||||||
pub fn commit(&self) {
|
pub fn commit(&self) {
|
||||||
let id = self.id as usize;
|
let id = self.loan.index as usize;
|
||||||
OBJECT_ATTRIBUTE_MEMORY.set((id + 0) * 4 + 3, self.attributes.p_a as u16);
|
OBJECT_ATTRIBUTE_MEMORY.set((id + 0) * 4 + 3, self.attributes.p_a as u16);
|
||||||
OBJECT_ATTRIBUTE_MEMORY.set((id + 1) * 4 + 3, self.attributes.p_b as u16);
|
OBJECT_ATTRIBUTE_MEMORY.set((id + 1) * 4 + 3, self.attributes.p_b as u16);
|
||||||
OBJECT_ATTRIBUTE_MEMORY.set((id + 2) * 4 + 3, self.attributes.p_c as u16);
|
OBJECT_ATTRIBUTE_MEMORY.set((id + 2) * 4 + 3, self.attributes.p_c as u16);
|
||||||
|
@ -227,8 +254,8 @@ impl ObjectControl {
|
||||||
unsafe { o.commit(index) };
|
unsafe { o.commit(index) };
|
||||||
}
|
}
|
||||||
ObjectControl {
|
ObjectControl {
|
||||||
object_count: 0,
|
objects: RefCell::new(Bitarray::new()),
|
||||||
affine_count: 0,
|
affines: RefCell::new(Bitarray::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,43 +273,59 @@ impl ObjectControl {
|
||||||
DISPLAY_CONTROL.set(disp);
|
DISPLAY_CONTROL.set(disp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an unused standard object. Currently dropping an unused object will
|
fn get_unused_object_index(&self) -> u8 {
|
||||||
/// not free this. You should either keep around all objects you need
|
let mut objects = self.objects.borrow_mut();
|
||||||
/// forever or drop and reobtain ObjectControl. Panics if more than 128
|
for index in 0..128 {
|
||||||
/// objects are obtained.
|
if !objects.get(index).unwrap() {
|
||||||
pub fn get_object_standard(&mut self) -> ObjectStandard {
|
objects.set(index, true);
|
||||||
let id = self.object_count;
|
return index as u8;
|
||||||
self.object_count += 1;
|
}
|
||||||
assert!(id < 128, "object id must be less than 128");
|
}
|
||||||
|
panic!("object id must be less than 128");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_unused_affine_index(&self) -> u8 {
|
||||||
|
let mut affines = self.affines.borrow_mut();
|
||||||
|
for index in 0..32 {
|
||||||
|
if !affines.get(index).unwrap() {
|
||||||
|
affines.set(index, true);
|
||||||
|
return index as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("affine id must be less than 32");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an unused standard object. Panics if more than 128 objects are
|
||||||
|
/// obtained.
|
||||||
|
pub fn get_object_standard(&self) -> ObjectStandard {
|
||||||
|
let id = self.get_unused_object_index();
|
||||||
ObjectStandard {
|
ObjectStandard {
|
||||||
attributes: ObjectAttribute::new(),
|
attributes: ObjectAttribute::new(),
|
||||||
id,
|
loan: ObjectLoan {
|
||||||
|
objects: &self.objects,
|
||||||
|
index: id,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an unused affine object. Currently dropping an unused object will
|
/// Get an unused affine object. Panics if more than 128 objects are
|
||||||
/// not free this. You should either keep around all objects you need
|
/// obtained.
|
||||||
/// forever or drop and reobtain ObjectControl. Panics if more than 128
|
pub fn get_object_affine(&self) -> ObjectAffine {
|
||||||
/// objects are obtained.
|
let id = self.get_unused_object_index();
|
||||||
pub fn get_object_affine(&mut self) -> ObjectAffine {
|
|
||||||
let id = self.object_count;
|
|
||||||
self.object_count += 1;
|
|
||||||
assert!(id < 128, "object id must be less than 128");
|
|
||||||
ObjectAffine {
|
ObjectAffine {
|
||||||
attributes: ObjectAttribute::new(),
|
attributes: ObjectAttribute::new(),
|
||||||
id,
|
loan: ObjectLoan {
|
||||||
|
objects: &self.objects,
|
||||||
|
index: id,
|
||||||
|
},
|
||||||
aff_id: None,
|
aff_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an unused affine matrix. Currently dropping an unused object will
|
/// Get an unused affine matrix. Panics if more than 32 affine matricies are
|
||||||
/// not free this. You should either keep around all affine matricies you
|
/// obtained.
|
||||||
/// need forever or drop and reobtain ObjectControl. Panics if more than 32
|
pub fn get_affine(&self) -> AffineMatrix {
|
||||||
/// affine matricies are obtained.
|
let id = self.get_unused_affine_index();
|
||||||
pub fn get_affine(&mut self) -> AffineMatrix {
|
|
||||||
let id = self.affine_count;
|
|
||||||
self.affine_count += 1;
|
|
||||||
assert!(id < 32, "affine id must be less than 32");
|
|
||||||
AffineMatrix {
|
AffineMatrix {
|
||||||
attributes: AffineMatrixAttributes {
|
attributes: AffineMatrixAttributes {
|
||||||
p_a: 0,
|
p_a: 0,
|
||||||
|
@ -290,7 +333,51 @@ impl ObjectControl {
|
||||||
p_c: 0,
|
p_c: 0,
|
||||||
p_d: 0,
|
p_d: 0,
|
||||||
},
|
},
|
||||||
id,
|
loan: AffineLoan {
|
||||||
|
affines: &self.affines,
|
||||||
|
index: id,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test_case]
|
||||||
|
fn get_and_release_object(gba: &mut crate::Gba) {
|
||||||
|
let gfx = gba.display.video.tiled0();
|
||||||
|
let objs = gfx.object;
|
||||||
|
|
||||||
|
let _o1 = {
|
||||||
|
let o0 = objs.get_object_standard();
|
||||||
|
let o1 = objs.get_object_standard();
|
||||||
|
assert_eq!(o0.loan.index, 0);
|
||||||
|
assert_eq!(o1.loan.index, 1);
|
||||||
|
o1
|
||||||
|
};
|
||||||
|
|
||||||
|
let o0 = objs.get_object_standard();
|
||||||
|
assert_eq!(o0.loan.index, 0);
|
||||||
|
let o2 = objs.get_object_affine();
|
||||||
|
assert_eq!(o2.loan.index, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case]
|
||||||
|
fn get_and_release_affine(gba: &mut crate::Gba) {
|
||||||
|
let gfx = gba.display.video.tiled0();
|
||||||
|
let objs = gfx.object;
|
||||||
|
|
||||||
|
let _a1 = {
|
||||||
|
let a0 = objs.get_affine();
|
||||||
|
let a1 = objs.get_affine();
|
||||||
|
assert_eq!(a0.loan.index, 0);
|
||||||
|
assert_eq!(a1.loan.index, 1);
|
||||||
|
a1
|
||||||
|
};
|
||||||
|
|
||||||
|
let a0 = objs.get_affine();
|
||||||
|
assert_eq!(a0.loan.index, 0);
|
||||||
|
let a2 = objs.get_affine();
|
||||||
|
assert_eq!(a2.loan.index, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -253,7 +253,7 @@ pub struct Tiled0 {
|
||||||
|
|
||||||
impl Tiled0 {
|
impl Tiled0 {
|
||||||
pub(crate) unsafe fn new() -> Self {
|
pub(crate) unsafe fn new() -> Self {
|
||||||
set_graphics_settings(GraphicsSettings::empty());
|
set_graphics_settings(GraphicsSettings::empty() | GraphicsSettings::SPRITE1_D);
|
||||||
set_graphics_mode(DisplayMode::Tiled0);
|
set_graphics_mode(DisplayMode::Tiled0);
|
||||||
Tiled0 {
|
Tiled0 {
|
||||||
used_blocks: 0,
|
used_blocks: 0,
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub mod input;
|
||||||
/// Implements sound output.
|
/// Implements sound output.
|
||||||
pub mod sound;
|
pub mod sound;
|
||||||
|
|
||||||
|
mod bitarray;
|
||||||
mod interrupt;
|
mod interrupt;
|
||||||
mod memory_mapped;
|
mod memory_mapped;
|
||||||
/// Implements logging to the mgba emulator.
|
/// Implements logging to the mgba emulator.
|
||||||
|
|
|
@ -18,7 +18,7 @@ enum Status {
|
||||||
|
|
||||||
fn test_file(file_to_run: &str) -> Status {
|
fn test_file(file_to_run: &str) -> Status {
|
||||||
let mut finished = Status::Running;
|
let mut finished = Status::Running;
|
||||||
let debug_reader_mutex = Regex::new(r"^\[(.*)\] GBA Debug: (.*)$").unwrap();
|
let debug_reader_mutex = Regex::new(r"(?s)^\[(.*)\] GBA Debug: (.*)$").unwrap();
|
||||||
|
|
||||||
let mut mgba = runner::MGBA::new(file_to_run);
|
let mut mgba = runner::MGBA::new(file_to_run);
|
||||||
let video_buffer = mgba.get_video_buffer();
|
let video_buffer = mgba.get_video_buffer();
|
||||||
|
|
Loading…
Reference in a new issue