feat: property-based simulation
parent
67512ff926
commit
6c1b68d1fd
|
|
@ -80,24 +80,21 @@ fn debug_painter(
|
|||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if key_input.just_pressed(KeyCode::Key1) {
|
||||
brush.tile = 1;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key2) {
|
||||
brush.tile = 2;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key3) {
|
||||
brush.tile = 3;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key4) {
|
||||
brush.tile = 4;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key5) {
|
||||
brush.tile = 5;
|
||||
}
|
||||
if key_input.just_pressed(KeyCode::Key6) {
|
||||
brush.tile = 6;
|
||||
|
||||
for (index, key) in vec![
|
||||
KeyCode::Key1,
|
||||
KeyCode::Key2,
|
||||
KeyCode::Key3,
|
||||
KeyCode::Key4,
|
||||
KeyCode::Key5,
|
||||
KeyCode::Key6,
|
||||
KeyCode::Key7,
|
||||
KeyCode::Key8,
|
||||
KeyCode::Key9,
|
||||
].iter().enumerate() {
|
||||
if key_input.just_pressed(*key) {
|
||||
brush.tile = index as u8 + 1;
|
||||
}
|
||||
}
|
||||
|
||||
let origin = Vector2I::from(world_pos);
|
||||
|
|
@ -116,8 +113,9 @@ fn debug_painter(
|
|||
for x in origin.x - (radius - 1)..origin.x + radius {
|
||||
let dx = (x - origin.x).abs();
|
||||
let dy = (y - origin.y).abs();
|
||||
let draw = dx * dx + dy * dy <= (radius - 1) * (radius - 1);
|
||||
|
||||
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
|
||||
if draw {
|
||||
let pos: Vector2I = Vector2I { x, y };
|
||||
debug_draw.line_colored(
|
||||
Vec3::from(pos) + Vec3::new(0.45, 0.45, 1.0),
|
||||
|
|
@ -127,12 +125,7 @@ fn debug_painter(
|
|||
);
|
||||
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
|
||||
{
|
||||
// 6 is special
|
||||
if id == 6 {
|
||||
terrain.mark_dirty(&pos)
|
||||
} else {
|
||||
terrain.set_texel(&pos, id, None)
|
||||
}
|
||||
terrain.set_texel(&pos, id, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
src/terrain2d.rs
158
src/terrain2d.rs
|
|
@ -94,6 +94,8 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
|
|||
{
|
||||
if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
|
||||
chunk.mark_clean();
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut y_range: Vec<_> = (rect.min.y..rect.max.y + 1).collect();
|
||||
let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect();
|
||||
|
|
@ -106,104 +108,65 @@ fn terrain_simulation(mut terrain: ResMut<Terrain2D>, frame_counter: Res<FrameCo
|
|||
}
|
||||
|
||||
for y in y_range.iter() {
|
||||
'texel_loop: for x in x_range.iter() {
|
||||
for x in x_range.iter() {
|
||||
let local = Vector2I::new(*x, *y);
|
||||
let global = local_to_global(&local, &chunk_index);
|
||||
|
||||
let texel = if let Some(texel) = terrain.get_texel(&global) {
|
||||
if texel.last_simulation == simulation_frame {
|
||||
continue 'texel_loop;
|
||||
}
|
||||
texel
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let tb = if let Some(tb) = TexelBehaviour2D::from_id(&texel.id) {
|
||||
tb
|
||||
} else {
|
||||
if terrain
|
||||
.get_texel(&global)
|
||||
.map_or(true, |t| t.last_simulation == simulation_frame)
|
||||
{
|
||||
continue;
|
||||
};
|
||||
|
||||
// TODO: generalise "check for empty space and move" behaviour
|
||||
match tb.form {
|
||||
TexelForm::Liquid => {
|
||||
// Check if there is space below
|
||||
{
|
||||
let below_pos = global + Vector2I::DOWN;
|
||||
if terrain.get_texel(&below_pos).map_or(true, |texel| {
|
||||
TexelBehaviour2D::is_empty(&texel.id)
|
||||
|| TexelBehaviour2D::is_gas(&texel.id)
|
||||
}) {
|
||||
let below_id =
|
||||
terrain.get_texel(&below_pos).map_or(0, |texel| texel.id);
|
||||
terrain.set_texel(&below_pos, texel.id, Some(simulation_frame));
|
||||
terrain.set_texel(&global, below_id, Some(simulation_frame));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is space to the side
|
||||
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
|
||||
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
|
||||
dirs.reverse();
|
||||
}
|
||||
for dir in dirs.iter() {
|
||||
let side_pos = global + *dir;
|
||||
if terrain.get_texel(&side_pos).map_or(true, |texel| {
|
||||
TexelBehaviour2D::is_empty(&texel.id)
|
||||
|| TexelBehaviour2D::is_gas(&texel.id)
|
||||
}) {
|
||||
let side_id =
|
||||
terrain.get_texel(&side_pos).map_or(0, |texel| texel.id);
|
||||
terrain.set_texel(&side_pos, texel.id, Some(simulation_frame));
|
||||
terrain.set_texel(&global, side_id, Some(simulation_frame));
|
||||
continue 'texel_loop;
|
||||
};
|
||||
}
|
||||
}
|
||||
TexelForm::Gas => {
|
||||
// Check if there is space above
|
||||
{
|
||||
let above_pos = global + Vector2I::UP;
|
||||
if terrain
|
||||
.get_texel(&above_pos)
|
||||
.map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id))
|
||||
{
|
||||
let above_id =
|
||||
terrain.get_texel(&above_pos).map_or(0, |texel| texel.id);
|
||||
terrain.set_texel(&above_pos, texel.id, Some(simulation_frame));
|
||||
terrain.set_texel(&global, above_id, Some(simulation_frame));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is space to the side
|
||||
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
|
||||
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
|
||||
dirs.reverse();
|
||||
}
|
||||
for dir in dirs.iter() {
|
||||
let side_pos = global + *dir;
|
||||
if terrain
|
||||
.get_texel(&side_pos)
|
||||
.map_or(true, |texel| TexelBehaviour2D::is_empty(&texel.id))
|
||||
{
|
||||
let side_id =
|
||||
terrain.get_texel(&side_pos).map_or(0, |texel| texel.id);
|
||||
terrain.set_texel(&side_pos, texel.id, Some(simulation_frame));
|
||||
terrain.set_texel(&global, side_id, Some(simulation_frame));
|
||||
continue 'texel_loop;
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
simulate_texel(global, &mut terrain, &frame_counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &FrameCounter) {
|
||||
let (_, behaviour) = match terrain.get_texel_behaviour(&global) {
|
||||
(Some(texel), Some(behaviour)) => (texel, behaviour),
|
||||
(_, _) => return,
|
||||
};
|
||||
|
||||
let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
|
||||
|
||||
// Gravity
|
||||
if let Some(gravity) = behaviour.gravity {
|
||||
let grav_offset = Vector2I::from(gravity);
|
||||
let grav_pos = global + grav_offset;
|
||||
|
||||
// Try falling
|
||||
{
|
||||
let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
|
||||
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
|
||||
terrain.swap_texels(&global, &grav_pos, Some(simulation_frame));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try "sliding"
|
||||
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
|
||||
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
|
||||
dirs.reverse();
|
||||
}
|
||||
for dir in dirs.iter() {
|
||||
let slide_pos = match behaviour.form {
|
||||
TexelForm::Solid => grav_pos + *dir,
|
||||
TexelForm::Liquid | TexelForm::Gas => global + *dir,
|
||||
};
|
||||
let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos);
|
||||
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
|
||||
terrain.swap_texels(&global, &slide_pos, Some(simulation_frame));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_terrain_events(
|
||||
mut terrain: ResMut<Terrain2D>,
|
||||
mut terrain_events: EventWriter<TerrainEvent2D>,
|
||||
|
|
@ -298,6 +261,17 @@ impl Terrain2D {
|
|||
.map_or(None, |chunk| chunk.get_texel(&global_to_local(global)))
|
||||
}
|
||||
|
||||
pub fn get_texel_behaviour(
|
||||
&self,
|
||||
global: &Vector2I,
|
||||
) -> (Option<Texel2D>, Option<TexelBehaviour2D>) {
|
||||
let texel = self.get_texel(global);
|
||||
(
|
||||
texel,
|
||||
texel.map_or(None, |t| TexelBehaviour2D::from_id(&t.id)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_texel(&mut self, global: &Vector2I, id: TexelID, simulation_frame: Option<u8>) {
|
||||
let index = global_to_chunk_index(global);
|
||||
let changed = match self.index_to_chunk_mut(&index) {
|
||||
|
|
@ -316,6 +290,18 @@ impl Terrain2D {
|
|||
self.mark_dirty(&(*global + Vector2I::LEFT));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_texels(
|
||||
&mut self,
|
||||
from_global: &Vector2I,
|
||||
to_global: &Vector2I,
|
||||
simulation_frame: Option<u8>,
|
||||
) {
|
||||
let from = self.get_texel(from_global).map_or(0, |t| t.id);
|
||||
let to = self.get_texel(to_global).map_or(0, |t| t.id);
|
||||
self.set_texel(to_global, from, simulation_frame);
|
||||
self.set_texel(from_global, to, simulation_frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_to_texel_index(position: &Vector2I) -> Option<usize> {
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ impl ChunkRect {
|
|||
|
||||
pub struct Chunk2D {
|
||||
pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize],
|
||||
// TODO: handle multiple dirty rects
|
||||
// TODO: handle multiple dirty rects?
|
||||
pub dirty_rect: Option<ChunkRect>,
|
||||
}
|
||||
|
||||
|
|
@ -212,13 +212,18 @@ impl Chunk2D {
|
|||
local_to_texel_index(position).map(|i| &mut self.texels[i])
|
||||
}
|
||||
|
||||
pub fn set_texel(&mut self, position: &Vector2I, id: TexelID, simulation_frame: Option<u8>) -> bool {
|
||||
pub fn set_texel(
|
||||
&mut self,
|
||||
position: &Vector2I,
|
||||
id: TexelID,
|
||||
simulation_frame: Option<u8>,
|
||||
) -> bool {
|
||||
let i = local_to_texel_index(position).expect("Texel index out of range");
|
||||
if self.texels[i].id != id {
|
||||
self.mark_dirty(position);
|
||||
}
|
||||
let update_neighbours =
|
||||
TexelBehaviour2D::is_solid(&self.texels[i].id) != TexelBehaviour2D::is_solid(&id);
|
||||
let update_neighbours = TexelBehaviour2D::has_collision(&self.texels[i].id)
|
||||
!= TexelBehaviour2D::has_collision(&id);
|
||||
let changed = self.texels[i].id != id;
|
||||
self.texels[i].id = id;
|
||||
if let Some(simulation_frame) = simulation_frame {
|
||||
|
|
@ -280,7 +285,8 @@ impl Chunk2D {
|
|||
| if local.x == 0 { 1 << 3 } else { 0 };
|
||||
|
||||
let mut sides: Vec<Segment2I>;
|
||||
if !TexelBehaviour2D::is_solid(&self.texels[i].id) {
|
||||
let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id);
|
||||
if !has_collision {
|
||||
sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize]
|
||||
.iter()
|
||||
.clone()
|
||||
|
|
@ -289,7 +295,7 @@ impl Chunk2D {
|
|||
to: side.to + local,
|
||||
})
|
||||
.collect();
|
||||
} else if TexelBehaviour2D::is_solid(&self.texels[i].id) && edge_mask != 0 {
|
||||
} else if has_collision && edge_mask != 0 {
|
||||
sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2);
|
||||
for i in 0..MST_EDGE_CASE_MAP.len() {
|
||||
if edge_mask & (1 << i) != 0 {
|
||||
|
|
@ -430,7 +436,7 @@ pub fn chunk_spawner(
|
|||
..default()
|
||||
},
|
||||
texture,
|
||||
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)),
|
||||
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 1.0)),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@ impl TerrainGen2D {
|
|||
|
||||
let mut id = 0;
|
||||
if value > 0.35 {
|
||||
id = 1;
|
||||
id = 11;
|
||||
}
|
||||
if value > 0.42 {
|
||||
id = 2;
|
||||
id = 12;
|
||||
}
|
||||
if value > 0.9 {
|
||||
id = 3;
|
||||
id = 13;
|
||||
}
|
||||
|
||||
chunk.set_texel(&local, id, None);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::util::Vector2I;
|
||||
|
||||
use super::TexelID;
|
||||
use bevy::prelude::*;
|
||||
use lazy_static::lazy_static;
|
||||
|
|
@ -7,32 +9,110 @@ lazy_static! {
|
|||
static ref ID_MAP: HashMap<TexelID, TexelBehaviour2D> = {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
result.insert(1, TexelBehaviour2D {
|
||||
color: Color::rgb(0.61, 0.49, 0.38),
|
||||
..default()
|
||||
});
|
||||
result.insert(
|
||||
1,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("loose sand"),
|
||||
color: Color::rgb(0.61, 0.49, 0.38),
|
||||
gravity: Some(TexelGravity::Down(100)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(2, TexelBehaviour2D {
|
||||
color: Color::rgb(0.21, 0.19, 0.17),
|
||||
..default()
|
||||
});
|
||||
result.insert(
|
||||
2,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("loose stone"),
|
||||
color: Color::rgb(0.21, 0.19, 0.17),
|
||||
gravity: Some(TexelGravity::Down(100)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(3, TexelBehaviour2D {
|
||||
color: Color::rgb(0.11, 0.11, 0.11),
|
||||
..default()
|
||||
});
|
||||
|
||||
result.insert(4, TexelBehaviour2D {
|
||||
color: Color::rgb(0.0, 0.0, 1.0),
|
||||
form: TexelForm::Liquid,
|
||||
..default()
|
||||
});
|
||||
|
||||
result.insert(5, TexelBehaviour2D {
|
||||
color: Color::rgb(0.0, 1.0, 0.0),
|
||||
form: TexelForm::Gas,
|
||||
..default()
|
||||
});
|
||||
result.insert(
|
||||
3,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("loose sturdy stone"),
|
||||
color: Color::rgb(0.11, 0.11, 0.11),
|
||||
gravity: Some(TexelGravity::Down(100)),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
4,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("water"),
|
||||
color: Color::rgba(0.0, 0.0, 1.0, 0.5),
|
||||
form: TexelForm::Liquid,
|
||||
gravity: Some(TexelGravity::Down(10)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
5,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("oil"),
|
||||
color: Color::rgba(0.0, 1.0, 0.0, 0.5),
|
||||
form: TexelForm::Gas,
|
||||
gravity: Some(TexelGravity::Up(50)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
6,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("gas"),
|
||||
color: Color::rgba(0.5, 0.5, 0.25, 0.5),
|
||||
form: TexelForm::Liquid,
|
||||
gravity: Some(TexelGravity::Down(5)),
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
11,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("sand"),
|
||||
color: Color::rgb(0.61, 0.49, 0.38),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
12,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("stone"),
|
||||
color: Color::rgb(0.21, 0.19, 0.17),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
13,
|
||||
TexelBehaviour2D {
|
||||
name: String::from("sturdy stone"),
|
||||
color: Color::rgb(0.11, 0.11, 0.11),
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result.insert(
|
||||
u8::MAX,
|
||||
TexelBehaviour2D {
|
||||
color: Color::BLACK,
|
||||
has_collision: true,
|
||||
..default()
|
||||
},
|
||||
);
|
||||
|
||||
result
|
||||
};
|
||||
|
|
@ -41,38 +121,86 @@ lazy_static! {
|
|||
#[derive(Clone, Copy, Default, PartialEq)]
|
||||
pub enum TexelForm {
|
||||
#[default]
|
||||
// Solid materials, when affected by gravity, create pyramid-like piles
|
||||
Solid,
|
||||
// Liquid materials, when affected by gravity, act like solids but also try to stabilise the surface level by traveling flat surfaces
|
||||
Liquid,
|
||||
// Gas materials act like liquids, but also have density/pressure that causes them to disperse
|
||||
Gas,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct TexelBehaviour2D {
|
||||
// pub flammability: Option<f32>,
|
||||
// pub gravity: Option<f32>,
|
||||
pub form: TexelForm,
|
||||
pub color: Color,
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum TexelGravity {
|
||||
Down(u8),
|
||||
Up(u8),
|
||||
}
|
||||
|
||||
// TODO: change form-based functions like is_solid to behaviour based (e.g. has_collision)
|
||||
impl From<TexelGravity> for Vector2I {
|
||||
fn from(gravity: TexelGravity) -> Self {
|
||||
match gravity {
|
||||
TexelGravity::Down(_) => Vector2I::DOWN,
|
||||
TexelGravity::Up(_) => Vector2I::UP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TexelBehaviour2D {
|
||||
pub name: String,
|
||||
pub color: Color,
|
||||
pub form: TexelForm,
|
||||
pub has_collision: bool,
|
||||
pub gravity: Option<TexelGravity>,
|
||||
pub toughness: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for TexelBehaviour2D {
|
||||
fn default() -> Self {
|
||||
TexelBehaviour2D {
|
||||
name: "Unnamed material".to_string(),
|
||||
color: Color::PINK,
|
||||
form: TexelForm::Solid,
|
||||
has_collision: false,
|
||||
gravity: None,
|
||||
toughness: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: change form-based functions like is_solid to behaviour based (e.g. has_collision)
|
||||
impl TexelBehaviour2D {
|
||||
pub fn from_id(id: &TexelID) -> Option<Self> {
|
||||
ID_MAP.get(id).copied()
|
||||
ID_MAP.get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn is_empty(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).is_none()
|
||||
}
|
||||
|
||||
pub fn is_solid(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Solid)
|
||||
pub fn has_collision(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).map_or(false, |b| b.has_collision)
|
||||
}
|
||||
|
||||
pub fn is_liquid(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Liquid)
|
||||
}
|
||||
pub fn can_displace(from: &TexelBehaviour2D, to: &Option<TexelBehaviour2D>) -> bool {
|
||||
let to = if let Some(to) = to { to } else { return true };
|
||||
|
||||
pub fn is_gas(id: &TexelID) -> bool {
|
||||
ID_MAP.get(id).map_or(false, |tb| tb.form == TexelForm::Gas)
|
||||
match (from.form, to.form) {
|
||||
(_, TexelForm::Solid) => false,
|
||||
(_, _) => {
|
||||
if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) {
|
||||
match (from_grav, to_grav) {
|
||||
(TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => {
|
||||
from_grav > to_grav
|
||||
}
|
||||
(TexelGravity::Up(from_grav), TexelGravity::Up(to_grav)) => {
|
||||
from_grav > to_grav
|
||||
}
|
||||
(_, _) => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue