diff --git a/Cargo.toml b/Cargo.toml index d21f14bf..c4b7279f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ concat-string = { version = "1.0.1", optional = true } [features] default = [ "acreplace", + "binary_space_partition", "cellularnoise", "dmi", "file", @@ -69,6 +70,7 @@ default = [ "json", "log", "noise", + "random_room_placement", "sql", "time", "toml", @@ -77,6 +79,7 @@ default = [ # default features acreplace = ["aho-corasick"] +binary_space_partition = ["rand", "rayon", "serde", "serde_json", "sha2"] cellularnoise = ["rand", "rayon"] dmi = ["png", "image"] file = [] @@ -84,6 +87,7 @@ git = ["git2", "chrono"] http = ["reqwest", "serde", "serde_json", "once_cell", "jobs"] json = ["serde", "serde_json"] log = ["chrono", "flume"] +random_room_placement = ["rand", "rayon", "serde", "serde_json", "sha2"] sql = ["mysql", "serde", "serde_json", "once_cell", "dashmap", "jobs"] time = [] toml = ["serde", "serde_json", "toml-dep"] diff --git a/README.md b/README.md index 91e439bc..a27cc64e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ To get additional features, pass a list to `--features`, for example `--features The default features are: * acreplace: Aho-Corasick string matching and replacement. +* binary_space_partition: Function to generate "rooms" more or less evenly distributed over a given area. * cellularnoise: Function to generate cellular automata-based noise. * dmi: DMI manipulations which are impossible from within BYOND. Used by the asset cache subsystem to improve load times. @@ -97,6 +98,7 @@ The default features are: * json: Function to check JSON validity. * log: Faster log output. * noise: 2d Perlin noise. +* random_room_placement: Function to generate "rooms" randomly placed in a given area, only taking care to not overlap one another. * sql: Asynchronous MySQL/MariaDB client library. * time: High-accuracy time measuring. * toml: TOML parser. diff --git a/dmsrc/binary-space-partition.dm b/dmsrc/binary-space-partition.dm new file mode 100644 index 00000000..30ba68c3 --- /dev/null +++ b/dmsrc/binary-space-partition.dm @@ -0,0 +1,24 @@ +/** + * This proc generates rooms in a specified area of random size and placement. Essential for procedurally generated areas, BSP works by cutting a given area in half, + * then cutting one of those subsections in half, and repeating this process until a minimum size is reached, then backtracking to other subsections that are not of + * the minimum size yet. These cuts are offset by small random amounts so that the sections are all varied in size and shape. + * + * BSP excels at creating rooms or areas with a relatively even distribution over an area, so there won't be too much blank open area. However if you discard rooms that + * overlap pre-existing map structures or areas, you may still get blank areas where nothing interesting appears. + * + * Return: + * * a json list of room data to be processed by json_decode in byond and further processed there. + * + * Arguments: + * * width: the width of the area to generate in + * * height: the height of the area to generate in + * * hash: the rng seed the generator will use for this instance + * * map_subsection_min_size: The minimum size of a map subsection. When using this for rooms with walls, the minimum possible square will be a 5x5 room. Barring walls, + * this will be a 3x3 room. The maximum size will be 9x9, because a further cut could reduce this size beneath the minimum size. + * * map_subsection_min_room_width: The minimum room width once the subsections are finalized. Room width and height are random between this amount, and the subsection + * max size + * * map_subsection_min_room_height: The minimum room height once the subsections are finalized. Room width and height are random between this amount, and the subsection + * max size + */ +#define rustg_bsp_generate(width, height, hash, map_subsection_min_size, map_subsection_min_room_width, map_subsection_min_room_height) \ + RUSTG_CALL(RUST_G, "bsp_generate")(width, height, hash, map_subsection_min_size, map_subsection_min_room_width, map_subsection_min_room_height) diff --git a/dmsrc/random-room-placement.dm b/dmsrc/random-room-placement.dm new file mode 100644 index 00000000..48e6ceb2 --- /dev/null +++ b/dmsrc/random-room-placement.dm @@ -0,0 +1,24 @@ +/** + * This proc generates rooms in a specified area of random size and placement. Used in procedural generation, but far less intensively than Binary Space Partitioning + * due to Random Room Placement being far more simple and unreliable for area coverage. These rooms will not overlap one another, but that is the only logic + * they do. The room dimensions returned by this call are hardcoded to be the dimensions of maint ruins so that I could sprinkle pre-generated areas over + * the binary space rooms that are random. + * These dimensions are: + * * 3x3 + * * 3x5 + * * 5x3 + * * 5x4 + * * 10x5 + * * 10x10 + * + * Return: + * * a json list of room data to be processed by json_decode in byond and further processed there. + * + * Arguments: + * * width: the width of the area to generate in + * * height: the height of the area to generate in + * * desired_room_count: the number of rooms you want generated and returned + * * hash: the rng seed the generator will use for this instance + */ +#define rustg_random_room_generate(width, height, desired_room_count, hash) \ + RUSTG_CALL(RUST_G, "random_room_generate")(width, height, desired_room_count, hash) diff --git a/src/binary_space_partition.rs b/src/binary_space_partition.rs new file mode 100644 index 00000000..0e1248e3 --- /dev/null +++ b/src/binary_space_partition.rs @@ -0,0 +1,455 @@ +use crate::error::Result; +use rand::prelude::*; +use rand::rngs::StdRng; +use rand::Rng; +use serde::Serialize; +use std::{cmp, fmt}; + +struct BspLevel { + level: Level, + map_subsection_min_size: usize, + map_subsection_min_room_width: usize, + map_subsection_min_room_height: usize, +} + +byond_fn!(fn bsp_generate(width, height, hash, map_subsection_min_size, map_subsection_min_room_width, map_subsection_min_room_height) { + bsp_gen(width, height, hash, map_subsection_min_size, map_subsection_min_room_width, map_subsection_min_room_height).ok() +}); + +fn bsp_gen( + width_as_str: &str, + height_as_str: &str, + hash_as_str: &str, + map_subsection_min_size_as_str: &str, + map_subsection_min_room_width_as_str: &str, + map_subsection_min_room_height_as_str: &str, +) -> Result { + let default_hash: u64 = rand::thread_rng().gen(); + let width = width_as_str.parse::()?; + let height = height_as_str.parse::()?; + let map_subsection_min_room_width = map_subsection_min_room_width_as_str.parse::()?; + let map_subsection_min_room_height = map_subsection_min_room_height_as_str.parse::()?; + + //map subsections that the BSP algorithm creates should never be smaller than the minimum desired room size they will contain. This will crash the server + let map_subsection_min_size = cmp::max( + map_subsection_min_size_as_str.parse::()?, + cmp::max( + map_subsection_min_room_width, + map_subsection_min_room_height, + ) + 1, + ); + + let mut rng: StdRng = SeedableRng::seed_from_u64( + hash_as_str + .parse::()? + .try_into() + .unwrap_or(default_hash), + ); + + let level = BspLevel::new( + width, + height, + &mut rng, + map_subsection_min_size, + map_subsection_min_room_width, + map_subsection_min_room_height, + ); + + Ok(serde_json::to_string(&level.rooms)?) +} + +impl BspLevel { + fn new( + width: usize, + height: usize, + rng: &mut StdRng, + map_subsection_min_size: usize, + map_subsection_min_room_width: usize, + map_subsection_min_room_height: usize, + ) -> Level { + let level = Level::new(width, height); + + let mut map = BspLevel { + level, + map_subsection_min_size, + map_subsection_min_room_width, + map_subsection_min_room_height, + }; + + map.place_rooms(rng); + + map.level + } + + fn place_rooms(&mut self, rng: &mut StdRng) { + let mut root = Leaf::new( + 0, + 0, + self.level.width, + self.level.height, + self.map_subsection_min_size, + self.map_subsection_min_room_width, + self.map_subsection_min_room_height, + ); + root.generate(rng); + + root.create_rooms(rng); + + for leaf in root.iter() { + if leaf.is_leaf() { + if let Some(room) = leaf.get_room() { + self.level.add_room(&room); + } + } + + for corridor in &leaf.corridors { + self.level.add_room(&corridor); + } + } + } +} + +struct Leaf { + min_size: usize, + x: usize, + y: usize, + width: usize, + height: usize, + min_room_width: usize, + min_room_height: usize, + left_child: Option>, + right_child: Option>, + room: Option, + corridors: Vec, +} + +impl Leaf { + fn new( + x: usize, + y: usize, + width: usize, + height: usize, + min_size: usize, + min_room_width: usize, + min_room_height: usize, + ) -> Self { + Leaf { + min_size, + x, + y, + width, + height, + min_room_width, + min_room_height, + left_child: None, + right_child: None, + room: None, + corridors: vec![], + } + } + + fn is_leaf(&self) -> bool { + self.left_child.is_none() && self.right_child.is_none() + } + + fn generate(&mut self, rng: &mut StdRng) { + if self.is_leaf() { + if self.split(rng) { + self.left_child.as_mut().unwrap().generate(rng); + self.right_child.as_mut().unwrap().generate(rng); + } + } + } + + fn split(&mut self, rng: &mut StdRng) -> bool { + // if width >25% height, split vertically + // if height >25% width, split horz + // otherwise random + + // this is the random choice + let mut split_horz = match rng.gen_range(0..2) { + 0 => false, + _ => true, + }; + + // then override with width/height check + if self.width > self.height && (self.width as f32 / self.height as f32) >= 1.25 { + split_horz = false; + } else if self.height > self.width && (self.height as f32 / self.width as f32) >= 1.25 { + split_horz = true; + } + + let max = match split_horz { + true => self.height - self.min_size, + false => self.width - self.min_size, + }; + + // the current area is small enough, so stop splitting + if max <= self.min_size { + return false; + } + + let split_pos = rng.gen_range(self.min_size..max); + if split_horz { + self.left_child = Some(Box::new(Leaf::new( + self.x, + self.y, + self.width, + split_pos, + self.min_size, + self.min_room_width, + self.min_room_height, + ))); + self.right_child = Some(Box::new(Leaf::new( + self.x, + self.y + split_pos, + self.width, + self.height - split_pos, + self.min_size, + self.min_room_width, + self.min_room_height, + ))); + } else { + self.left_child = Some(Box::new(Leaf::new( + self.x, + self.y, + split_pos, + self.height, + self.min_size, + self.min_room_width, + self.min_room_height, + ))); + self.right_child = Some(Box::new(Leaf::new( + self.x + split_pos, + self.y, + self.width - split_pos, + self.height, + self.min_size, + self.min_room_width, + self.min_room_height, + ))); + } + + true + } + + fn create_rooms(&mut self, rng: &mut StdRng) { + if let Some(ref mut room) = self.left_child { + room.as_mut().create_rooms(rng); + }; + + if let Some(ref mut room) = self.right_child { + room.as_mut().create_rooms(rng); + }; + + // if last level, add a room + if self.is_leaf() { + let width = rng.gen_range(self.min_room_width..=self.width); + let height = rng.gen_range(self.min_room_height..=self.height); + let x = rng.gen_range(0..=self.width - width); + let y = rng.gen_range(0..=self.height - height); + + self.room = Some(Room::new( + format!("bsp room"), + x + self.x, + y + self.y, + width, + height, + )); + } + } + + fn get_room(&self) -> Option { + if self.is_leaf() { + return self.room.clone(); + } + + let mut left_room: Option = None; + let mut right_room: Option = None; + + if let Some(ref room) = self.left_child { + left_room = room.get_room(); + } + + if let Some(ref room) = self.right_child { + right_room = room.get_room(); + } + + match (left_room, right_room) { + (None, None) => None, + (Some(room), _) => Some(room), + (_, Some(room)) => Some(room), + } + } + + fn iter(&self) -> LeafIterator { + LeafIterator::new(&self) + } +} + +struct LeafIterator<'a> { + current_node: Option<&'a Leaf>, + right_nodes: Vec<&'a Leaf>, +} + +impl<'a> LeafIterator<'a> { + fn new(root: &'a Leaf) -> LeafIterator<'a> { + let mut iter = LeafIterator { + right_nodes: vec![], + current_node: None, + }; + + iter.add_subtrees(root); + iter + } + + // set the current node to the one provided + // and add any child leaves to the node vec + fn add_subtrees(&mut self, node: &'a Leaf) { + if let Some(ref left) = node.left_child { + self.right_nodes.push(&*left); + } + if let Some(ref right) = node.right_child { + self.right_nodes.push(&*right); + } + + self.current_node = Some(node); + } +} + +impl<'a> Iterator for LeafIterator<'a> { + type Item = &'a Leaf; + + fn next(&mut self) -> Option { + let result = self.current_node.take(); + if let Some(rest) = self.right_nodes.pop() { + self.add_subtrees(rest); + } + + match result { + Some(leaf) => Some(&*leaf), + None => None, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TileType { + Space = 0, + Floor = 1, + Wall = 2, +} +pub struct Level { + width: usize, + height: usize, + board: Vec>, + rooms: Vec, + increment: usize, +} + +impl Level { + fn new(width: usize, height: usize) -> Self { + let mut new_level = Level { + width, + height, + board: Vec::new(), + rooms: Vec::new(), + increment: 0, + }; + new_level.update_board(); + new_level + } + + fn update_board(&mut self) -> Vec> { + let mut new_board = Vec::new(); + self.increment += 1; + for _ in 0..self.height { + let gen_floor_first = true; + + let mut row = vec![TileType::Floor as usize; self.width as usize]; + if !gen_floor_first { + row = vec![TileType::Space as usize; self.width as usize]; + } + + new_board.push(row); + } + for room in &self.rooms { + for row in 0..room.height { + for col in 0..room.width { + let y = (room.y + row) as usize; + let x = (room.x + col) as usize; + if row == 0 || col == 0 || row == room.height - 1 || col == room.width - 1 { + // might just let byond handle the walls + new_board[y][x] = TileType::Wall as usize; + } else { + new_board[y][x] = TileType::Floor as usize; + } + } + } + } + self.board = new_board.clone(); + new_board + } + + fn add_room(&mut self, room: &Room) { + self.rooms.push(room.clone()); + self.update_board(); + } +} + +impl fmt::Display for Level { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for row in 0..self.height as usize { + for col in 0..self.width as usize { + write!(f, "{}", self.board[row][col])? + } + write!(f, "\n")? + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Point { + x: usize, + y: usize, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Room { + id: String, + x: usize, + y: usize, + x2: usize, + y2: usize, + width: usize, + height: usize, + center: Point, +} + +impl Room { + pub fn new(id: String, x: usize, y: usize, width: usize, height: usize) -> Self { + Room { + id, + x, + y, + x2: x + width, + y2: y + height, + width, + height, + center: Point { + x: x + (width / 2), + y: y + (height / 2), + }, + } + } + + pub fn intersects(&self, other: &Self) -> bool { + self.x <= other.x2 && self.x2 >= other.x && self.y <= other.y2 && self.y2 >= other.y + } + pub fn get_distance_to(&self, other: &Point) -> usize { + (((other.x - self.center.x).pow(2) + (other.y - self.center.y).pow(2)) as f64).sqrt() + as usize + } +} diff --git a/src/lib.rs b/src/lib.rs index c6c07e9e..e3ed4024 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ mod jobs; #[cfg(feature = "acreplace")] pub mod acreplace; +#[cfg(feature = "binary_space_partition")] +pub mod binary_space_partition; #[cfg(feature = "cellularnoise")] pub mod cellularnoise; #[cfg(feature = "dbpnoise")] @@ -37,6 +39,8 @@ pub mod log; pub mod noise_gen; #[cfg(feature = "pathfinder")] pub mod pathfinder; +#[cfg(feature = "random_room_placement")] +pub mod random_room_placement; #[cfg(feature = "redis_pubsub")] pub mod redis_pubsub; #[cfg(feature = "sql")] diff --git a/src/random_room_placement.rs b/src/random_room_placement.rs new file mode 100644 index 00000000..c91ca2fc --- /dev/null +++ b/src/random_room_placement.rs @@ -0,0 +1,260 @@ +use crate::error::Result; +use rand::distributions::WeightedIndex; +use rand::prelude::*; +use rand::rngs::StdRng; +use rand::Rng; +use serde::Serialize; +use std::fmt; + +struct RandomRoomLevel { + level: Level, +} + +byond_fn!(fn random_room_generate(width, height, desired_room_count, hash) { + random_room_gen(width, height, desired_room_count, hash).ok() +}); + +fn random_room_gen( + width_as_str: &str, + height_as_str: &str, + desired_room_count_as_str: &str, + hash_as_str: &str, +) -> Result { + let default_hash: u64 = rand::thread_rng().gen(); + let width = width_as_str.parse::()?; + let height = height_as_str.parse::()?; + let desired_room_count = desired_room_count_as_str.parse::()?; + + let mut rng: StdRng = SeedableRng::seed_from_u64( + hash_as_str + .parse::()? + .try_into() + .unwrap_or(default_hash), + ); + + let level = RandomRoomLevel::new(width, height, desired_room_count, &mut rng); + + Ok(serde_json::to_string(&level.rooms)?) +} + +impl RandomRoomLevel { + fn new(width: usize, height: usize, desired_room_count: usize, rng: &mut StdRng) -> Level { + let level = Level::new(width, height); + + let mut map = RandomRoomLevel { level }; + + map.place_rooms_random(desired_room_count, rng); + map.level + } + + fn place_rooms_random(&mut self, desired_room_count: usize, rng: &mut StdRng) { + let max_rooms = desired_room_count as usize; + let max_attempts = 15; + let mut attempts = 0; + while self.level.rooms.len() <= max_rooms && attempts <= max_attempts { + attempts += 1; + let mut x = rng.gen_range(0..self.level.width); + let mut y = rng.gen_range(0..self.level.height); + + let choices = [ + RoomDimensions::Maint3x3, + RoomDimensions::Maint3x5, + RoomDimensions::Maint5x3, + RoomDimensions::Maint5x4, + RoomDimensions::Maint10x5, + RoomDimensions::Maint10x10, + ]; + let weights = [4, 3, 4, 3, 2, 1]; + let dist = WeightedIndex::new(&weights).unwrap(); + //let mut rng = thread_rng(); + let room_layout = &choices[dist.sample(rng)]; + let width = room_layout.get_width(); + let height = room_layout.get_height(); + + if x + width > self.level.width { + x = self.level.width - width; + } + + if y + height > self.level.height { + y = self.level.height - height; + } + + let mut collides = false; + + let room = Room::new( + format!("RRPS room: {}", self.level.rooms.len()), + x, + y, + width, + height, + ); + + for other_room in &self.level.rooms { + if room.intersects(&other_room) { + collides = true; + break; + } + } + + if !collides { + self.level.add_room(&room); + attempts = 0; + } + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum TileType { + Space = 0, + Floor = 1, + Wall = 2, +} + +pub struct Level { + width: usize, + height: usize, + board: Vec>, + rooms: Vec, + increment: usize, +} + +impl Level { + fn new(width: usize, height: usize) -> Self { + let mut new_level = Level { + width, + height, + board: Vec::new(), + rooms: Vec::new(), + increment: 0, + }; + new_level.update_board(); + new_level + } + + fn update_board(&mut self) -> Vec> { + let mut new_board = Vec::new(); + self.increment += 1; + for _ in 0..self.height { + let gen_floor_first = true; + + let mut row = vec![TileType::Floor as usize; self.width as usize]; + if !gen_floor_first { + row = vec![TileType::Space as usize; self.width as usize]; + } + + new_board.push(row); + } + for room in &self.rooms { + for row in 0..room.height { + for col in 0..room.width { + let y = (room.y + row) as usize; + let x = (room.x + col) as usize; + if row == 0 || col == 0 || row == room.height - 1 || col == room.width - 1 { + // might just let byond handle the walls + new_board[y][x] = TileType::Wall as usize; + } else { + new_board[y][x] = TileType::Floor as usize; + } + } + } + } + self.board = new_board.clone(); + new_board + } + + fn add_room(&mut self, room: &Room) { + self.rooms.push(room.clone()); + self.update_board(); + } +} + +impl fmt::Display for Level { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for row in 0..self.height as usize { + for col in 0..self.width as usize { + write!(f, "{}", self.board[row][col])? + } + write!(f, "\n")? + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +pub enum RoomDimensions { + Maint3x3, + Maint3x5, + Maint5x3, + Maint5x4, + Maint10x5, + Maint10x10, +} +impl RoomDimensions { + fn get_height(&self) -> usize { + return match *self { + RoomDimensions::Maint3x3 => 3, + RoomDimensions::Maint3x5 => 5, + RoomDimensions::Maint5x3 => 3, + RoomDimensions::Maint5x4 => 4, + RoomDimensions::Maint10x5 => 5, + RoomDimensions::Maint10x10 => 10, + } + 2; //add 2 because the dimensions are equal to the inside of the room, and we need the dimensions with the walls in mind + } + + fn get_width(&self) -> usize { + return match *self { + RoomDimensions::Maint3x3 => 3, + RoomDimensions::Maint3x5 => 3, + RoomDimensions::Maint5x3 => 5, + RoomDimensions::Maint5x4 => 5, + RoomDimensions::Maint10x5 => 10, + RoomDimensions::Maint10x10 => 10, + } + 2; //add 2 because the dimensions are equal to the inside of the room, and we need the dimensions with the walls in mind + } +} + +#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Point { + x: usize, + y: usize, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Room { + id: String, + x: usize, + y: usize, + x2: usize, + y2: usize, + width: usize, + height: usize, + center: Point, +} + +impl Room { + pub fn new(id: String, x: usize, y: usize, width: usize, height: usize) -> Self { + Room { + id, + x, + y, + x2: x + width, + y2: y + height, + width, + height, + center: Point { + x: x + (width / 2), + y: y + (height / 2), + }, + } + } + + pub fn intersects(&self, other: &Self) -> bool { + self.x <= other.x2 && self.x2 >= other.x && self.y <= other.y2 && self.y2 >= other.y + } + pub fn get_distance_to(&self, other: &Point) -> usize { + (((other.x - self.center.x).pow(2) + (other.y - self.center.y).pow(2)) as f64).sqrt() + as usize + } +}