use std::{collections::HashMap, path::Path}; use anyhow::Result; use engine::prelude::cgmath::{Rad, Vector2}; // sql use rusqlite::{ types::{ToSql, Type}, Connection, Error, }; use super::mapdata::Coordinate; struct DBMetaInfo { name: String, version: String, width: u32, height: u32, } pub struct DBTextureInfo { pub index: u32, pub name: String, } pub struct DBEntityInfo { pub x_tile: u32, pub y_tile: u32, pub world: Vector2, pub rotation: Rad, pub entity: String, } pub struct DBNPCSpawnInfo { pub x_tile: u32, pub y_tile: u32, pub radius: f32, pub min_count: u32, pub max_count: u32, pub normal_npc: String, pub elite_npc: String, } pub struct DBBossSpawnInfo { pub x_tile: u32, pub y_tile: u32, pub boss: String, } #[derive(Clone)] pub struct EntityDBType { table_name: String, } impl EntityDBType { pub fn entity() -> Self { Self { table_name: "entitypositions".to_string(), } } pub fn spawn_location() -> Self { Self { table_name: "spawnlocations".to_string(), } } pub fn leave_location() -> Self { Self { table_name: "leavelocations".to_string(), } } } pub struct MapDBVersions; #[allow(unused)] impl MapDBVersions { const VERSION_0_1_0: &'static str = "0.1.0"; const VERSION_0_1_1: &'static str = "0.1.1"; const VERSION_0_2_0: &'static str = "0.2.0"; } pub struct MapDataBase { sql: Connection, name: String, version: String, width: u32, height: u32, } impl MapDataBase { pub fn new(path: impl AsRef, name: &str, width: u32, height: u32) -> Result { let mut me = Self::open_file(path)?; me.create_tables()?; me.init_default_values(name, width, height)?; me.read_meta_data()?; Ok(me) } pub fn load(path: impl AsRef) -> Result { let mut me = Self::open_file(path)?; me.check_version()?; me.read_meta_data()?; Ok(me) } fn open_file(path: impl AsRef) -> Result { Ok(Self { sql: Connection::open(path)?, name: Default::default(), version: Default::default(), width: 0, height: 0, }) } } impl MapDataBase { pub fn name(&self) -> &str { &self.name } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } } impl MapDataBase { fn init_default_values(&self, name: &str, width: u32, height: u32) -> Result<()> { // meta data self.sql.execute( "INSERT INTO meta (name, version, width, height) VALUES (?1, ?2, ?3, ?4)", &[ &name.to_string() as &dyn ToSql, &MapDBVersions::VERSION_0_1_1.to_string() as &dyn ToSql, &width, &height, ], )?; // tiles let tile_count = width * height; let mut insert_tiles = "INSERT INTO tiles (id, texture) VALUES ".to_string(); for i in 0..tile_count { if i != 0 { insert_tiles.push(','); } insert_tiles.push_str(format!("({}, 1)", i + 1).as_str()); } self.sql.execute(insert_tiles.as_str(), [])?; // heights let height_point_count = (width + 1) * (height + 1); let mut insert_height_points = "INSERT INTO heights (id, height) VALUES".to_string(); for i in 0..height_point_count { if i != 0 { insert_height_points.push(','); } insert_height_points.push_str(format!("({}, 0.0)", i + 1).as_str()); } self.sql.execute(insert_height_points.as_str(), [])?; // textures self.sql.execute( "INSERT INTO textures (id, name) VALUES (1, ?1)", &[&"grass"], )?; Ok(()) } fn create_tables(&self) -> Result<()> { // -------------------------------------------------------- // -------- meta information (name, width, height) -------- // -------------------------------------------------------- self.create_meta_table()?; // -------------------------------------------------------- // --- tiles information (which tile has which texture) --- // -------------------------------------------------------- self.create_tiles_table()?; // -------------------------------------------------------- // ------------ textures (name of the texture) ------------ // -------------------------------------------------------- self.create_textures_table()?; // -------------------------------------------------------- // ----------------------- heights ------------------------ // -------------------------------------------------------- self.create_heights_table()?; // -------------------------------------------------------- // ------------------- entity positions ------------------- // -------------------------------------------------------- self.create_entity_positions_table()?; self.create_spawn_table()?; self.create_leave_table()?; self.create_mob_spawn_table()?; self.create_boss_spawn_table()?; Ok(()) } fn create_meta_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS meta ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, version TEXT NOT NULL, width INTEGER NOT NULL, height INTEGER NOT NULL )", [], )?; self.sql.execute("DELETE FROM meta", [])?; Ok(()) } fn create_tiles_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS tiles ( id INTEGER PRIMARY KEY, texture INTEGER NOT NULL )", [], )?; self.sql.execute("DELETE FROM tiles", [])?; Ok(()) } fn create_textures_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS textures ( id INTEGER PRIMARY KEY, name TEXT NOT NULL )", [], )?; self.sql.execute("DELETE FROM textures", [])?; Ok(()) } fn create_heights_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS heights ( id INTEGER PRIMARY KEY, height REAL NOT NULL )", [], )?; self.sql.execute("DELETE FROM heights", [])?; Ok(()) } fn create_entity_positions_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS entitypositions ( x_tile INTEGER NOT NULL, y_tile INTEGER NOT NULL, entity_id TEXT NOT NULL, x_world REAL NOT NULL, y_world REAL NOT NULL, rotation REAL NOT NULL, primary key (x_tile, y_tile) )", [], )?; self.sql.execute("DELETE FROM entitypositions", [])?; Ok(()) } fn create_spawn_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS spawnlocations ( x_tile INTEGER NOT NULL, y_tile INTEGER NOT NULL, entity_id TEXT NOT NULL, x_world REAL NOT NULL, y_world REAL NOT NULL, rotation REAL NOT NULL, primary key (x_tile, y_tile) )", [], )?; self.sql.execute("DELETE FROM spawnlocations", [])?; Ok(()) } fn create_leave_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS leavelocations ( x_tile INTEGER NOT NULL, y_tile INTEGER NOT NULL, entity_id TEXT NOT NULL, x_world REAL NOT NULL, y_world REAL NOT NULL, rotation REAL NOT NULL, primary key (x_tile, y_tile) )", [], )?; self.sql.execute("DELETE FROM leavelocations", [])?; Ok(()) } fn create_mob_spawn_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS mobspawnlocations ( x_tile INTEGER NOT NULL, y_tile INTEGER NOT NULL, radius REAL NOT NULL, normal_npc TEXT, elite_npc TEXT, min_count INTEGER, max_count INTEGER, primary key (x_tile, y_tile) )", [], )?; self.sql.execute("DELETE FROM mobspawnlocations", [])?; Ok(()) } fn create_boss_spawn_table(&self) -> Result<()> { self.sql.execute( "CREATE TABLE IF NOT EXISTS bossspawnlocations ( x_tile INTEGER NOT NULL, y_tile INTEGER NOT NULL, boss TEXT, primary key (x_tile, y_tile) )", [], )?; self.sql.execute("DELETE FROM bossspawnlocations", [])?; Ok(()) } } impl MapDataBase { fn read_meta_data(&mut self) -> Result<()> { let info = self.sql.query_row( "SELECT name, version, width, height FROM meta WHERE id=1", [], |row| { Ok(DBMetaInfo { name: row.get(0)?, version: row.get(1)?, width: row.get(2)?, height: row.get(3)?, }) }, )?; self.name = info.name; self.version = info.version; self.width = info.width; self.height = info.height; Ok(()) } pub fn read_tiles(&self) -> Result> { let mut tiles_stmt = self.sql.prepare("SELECT * FROM tiles")?; let mapped_tiles = tiles_stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?; let mut tiles = HashMap::new(); for row in mapped_tiles { let (id, tile_id) = row?; tiles.insert(id, tile_id); } Ok(tiles) } pub fn read_heights(&self) -> Result> { let mut height_stmt = self.sql.prepare("SELECT * FROM heights")?; let mapped_heights = height_stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?; let mut heights = HashMap::new(); for row in mapped_heights { let (id, height) = row?; heights.insert(id, height); } Ok(heights) } pub fn read_textures(&self) -> Result> { let mut texture_stmt = self.sql.prepare("SELECT * FROM textures")?; let mapped_textures = texture_stmt.query_map([], |row| { Ok(DBTextureInfo { index: row.get(0)?, name: row.get(1)?, }) })?; let mut textures = Vec::new(); for row in mapped_textures { textures.push(row?); } Ok(textures) } fn read_ent_pos(&self, table_name: &str) -> Result> { let mut entity_pos_stmt = self.sql.prepare(&format!("SELECT * FROM {}", table_name))?; let mapped_entity_pos = entity_pos_stmt.query_map([], |row| { Ok(DBEntityInfo { x_tile: row.get(0)?, y_tile: row.get(1)?, world: Vector2::new(row.get(3)?, row.get(4)?), rotation: Rad(row.get(5)?), entity: row.get(2)?, }) })?; let mut entity_positions = Vec::new(); for row in mapped_entity_pos { entity_positions.push(row?); } Ok(entity_positions) } pub fn read_entities(&self) -> Result> { self.read_ent_pos("entitypositions") } pub fn read_spawn_locations(&self) -> Result> { self.read_ent_pos("spawnlocations") } pub fn read_leave_locations(&self) -> Result> { self.read_ent_pos("leavelocations") } pub fn read_mob_spawn_locations(&self) -> Result> { let mut entity_pos_stmt = self.sql.prepare("SELECT * FROM mobspawnlocations")?; let mapped_entity_pos = entity_pos_stmt.query_map([], |row| { Ok(DBNPCSpawnInfo { x_tile: row.get(0)?, y_tile: row.get(1)?, radius: row.get(2)?, min_count: row.get(5)?, max_count: row.get(6)?, normal_npc: match row.get(3) { Ok(npc) => npc, Err(err) => match &err { Error::InvalidColumnType(_, _, row_type) => match row_type { Type::Null => String::new(), _ => return Err(err), }, _ => return Err(err), }, }, elite_npc: match row.get(4) { Ok(npc) => npc, Err(err) => match &err { Error::InvalidColumnType(_, _, row_type) => match row_type { Type::Null => String::new(), _ => return Err(err), }, _ => return Err(err), }, }, }) })?; let mut entity_positions = Vec::new(); for row in mapped_entity_pos { entity_positions.push(row?); } Ok(entity_positions) } pub fn read_boss_spawn_locations(&self) -> Result> { let mut boss_pos_stmt = self.sql.prepare("SELECT * FROM bossspawnlocations")?; let mapped_boss_pos = boss_pos_stmt.query_map([], |row| { Ok(DBBossSpawnInfo { x_tile: row.get(0)?, y_tile: row.get(1)?, boss: match row.get(2) { Ok(boss) => boss, Err(err) => match &err { Error::InvalidColumnType(_, _, row_type) => match row_type { Type::Null => String::new(), _ => return Err(err), }, _ => return Err(err), }, }, }) })?; let mut boss_positions = Vec::new(); for row in mapped_boss_pos { boss_positions.push(row?); } Ok(boss_positions) } } impl MapDataBase { pub fn insert_npc_spawn( &self, tile: (u32, u32), radius: f32, min_count: u32, max_count: u32, ) -> Result<()> { let params: &[&dyn ToSql] = &[&tile.0, &tile.1, &(radius as f64), &min_count, &max_count]; self.sql.execute( "INSERT INTO mobspawnlocations (x_tile, y_tile, radius, min_count, max_count) VALUES (?1, ?2, ?3, ?4, ?5)", params, )?; Ok(()) } pub fn remove_npc_spawn(&self, tile: (u32, u32)) -> Result<()> { self.sql.execute( "DELETE FROM mobspawnlocations WHERE x_tile=?1 AND y_tile=?2", &[&tile.0, &tile.1], )?; Ok(()) } pub fn insert_boss_spawn(&self, tile: (u32, u32)) -> Result<()> { self.sql.execute( "INSERT INTO bossspawnlocations (x_tile, y_tile) VALUES (?1, ?2)", [&tile.0, &tile.1], )?; Ok(()) } pub fn remove_boss_spawn(&self, tile: (u32, u32)) -> Result<()> { self.sql.execute( "DELETE FROM bossspawnlocations WHERE x_tile=?1 AND y_tile=?2", &[&tile.0, &tile.1], )?; Ok(()) } pub fn update_boss_name(&self, coordinate: &Coordinate, name: &str) -> Result<()> { self.sql.execute( "UPDATE bossspawnlocations SET boss=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &name as &dyn ToSql], )?; Ok(()) } pub fn insert_entity( &self, entity_db_type: EntityDBType, tile: (u32, u32), entity_name: String, position: Vector2, rotation: Rad, ) -> Result<()> { self.sql.execute( &format!( "INSERT INTO {} (x_tile, y_tile, entity_id, x_world, y_world, rotation) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", entity_db_type.table_name ), &[ &tile.0, &tile.1, &entity_name as &dyn ToSql, &(position.x as f64), &(position.y as f64), &(rotation.0 as f64), ], )?; Ok(()) } pub fn remove_entity(&self, entity_db_type: EntityDBType, tile: (u32, u32)) -> Result<()> { self.sql.execute( &format!( "DELETE FROM {} WHERE x_tile=?1 AND y_tile=?2", entity_db_type.table_name ), &[&tile.0, &tile.1], )?; Ok(()) } pub fn set_texture(&self, tile_index: u32, texture_index: u32) -> Result<()> { self.sql.execute( "UPDATE tiles SET texture=?1 WHERE id=?2", &[&texture_index, &tile_index], )?; Ok(()) } pub fn insert_new_texture(&self, texture_index: u32, texture_name: String) -> Result<()> { self.sql.execute( "INSERT INTO textures (id, name) VALUES (?1, ?2)", &[&texture_index, &texture_name as &dyn ToSql], )?; Ok(()) } pub fn remove_texture(&self, texture_index: u32) -> Result<()> { self.sql.execute( "DELETE FROM textures WHERE id=?1", &[&texture_index], )?; Ok(()) } pub fn update_texture_index(&self, broken_index: u32, last_index: u32) -> Result<()> { self.sql.execute( "UPDATE tiles SET texture=?1 WHERE texture=?2", &[&broken_index, &last_index], )?; self.sql.execute( "UPDATE textures SET id=?1 WHERE id=?2", &[&broken_index, &last_index], )?; Ok(()) } pub fn update_height(&self, index: u32, height: f32) -> Result<()> { self.sql.execute( "UPDATE heights SET height=?1 WHERE id=?2", &[&(height as f64), &index as &dyn ToSql], )?; Ok(()) } pub fn update_npc_spawn_radius(&self, coordinate: &Coordinate, radius: f32) -> Result<()> { self.sql.execute( "UPDATE mobspawnlocations SET radius=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &(radius as f64) as &dyn ToSql], )?; Ok(()) } pub fn update_npc_spawn_min_npc_count( &self, coordinate: &Coordinate, min_npc_count: u32, ) -> Result<()> { self.sql.execute( "UPDATE mobspawnlocations SET min_count=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &min_npc_count], )?; Ok(()) } pub fn update_npc_spawn_max_npc_count( &self, coordinate: &Coordinate, max_npc_count: u32, ) -> Result<()> { self.sql.execute( "UPDATE mobspawnlocations SET max_count=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &max_npc_count], )?; Ok(()) } pub fn update_npc_spawn_normal_npc( &self, coordinate: &Coordinate, normal_npc: String, ) -> Result<()> { self.sql.execute( "UPDATE mobspawnlocations SET normal_npc=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &normal_npc as &dyn ToSql], )?; Ok(()) } pub fn update_npc_spawn_elite_npc( &self, coordinate: &Coordinate, elite_npc: String, ) -> Result<()> { self.sql.execute( "UPDATE mobspawnlocations SET elite_npc=?3 WHERE x_tile=?1 AND y_tile=?2", &[&coordinate.x, &coordinate.y, &elite_npc as &dyn ToSql], )?; Ok(()) } } impl MapDataBase { fn get_version(&self) -> Result { Ok( match self .sql .query_row("SELECT version FROM meta WHERE id=1", [], |row| row.get(0)) { Ok(version) => version, Err(err) => { // everything before version 0.1.1 did not have a version and can be considered as version 0.1.0 if let Error::SqliteFailure(_sqlite_error, msg) = &err { if let Some(msg) = msg { if msg.contains("version") { return Ok(MapDBVersions::VERSION_0_1_0.to_string()); } } } return Err(err); } }, ) } fn check_version(&self) -> Result<()> { let mut version = self.get_version()?; // incrementally upgrade to the current version loop { match version.as_str() { MapDBVersions::VERSION_0_1_0 => { // update mob spawn table { // Add new columns to the table self.sql.execute( "ALTER TABLE mobspawnlocations ADD COLUMN radius REAL", [], )?; self.sql.execute( "ALTER TABLE mobspawnlocations ADD COLUMN normal_npc TEXT", [], )?; self.sql.execute( "ALTER TABLE mobspawnlocations ADD COLUMN elite_npc TEXT", [], )?; self.sql.execute( "ALTER TABLE mobspawnlocations ADD COLUMN min_count INTEGER", [], )?; self.sql.execute( "ALTER TABLE mobspawnlocations ADD COLUMN max_count INTEGER", [], )?; // create default values that were used at this time let radius: f32 = 5.0; let min_count: u32 = 3; let max_count: u32 = 7; // fill new data self.sql.execute( "UPDATE mobspawnlocations SET radius = ?1, min_count = ?2, max_count = ?3;", &[&radius as &dyn ToSql, &min_count, &max_count], )?; } // update meta table, it is possible that version 0.1.0 has no version inside the meta table // thus the old information is read, the table is recreated and the data is inserted back into it { // read old meta data table let (name, width, height): (String, u32, u32) = self.sql.query_row( "SELECT name, width, height FROM meta WHERE id=1", [], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), )?; // remove old meta table self.sql.execute("DROP TABLE meta", [])?; // create new meta table self.create_meta_table()?; // insert meta information back into table self.sql.execute( "INSERT INTO meta (name, version, width, height) VALUES (?1, ?2, ?3, ?4)", &[ &name, &MapDBVersions::VERSION_0_1_1.to_string() as &dyn ToSql, &width, &height, ], )?; } // update version string version = MapDBVersions::VERSION_0_1_1.to_string(); } MapDBVersions::VERSION_0_1_1 => { // add boss spawn table self.create_boss_spawn_table()?; // update version in meta table self.sql.execute( "UPDATE meta SET version = ?1;", [&MapDBVersions::VERSION_0_2_0.to_string() as &dyn ToSql], )?; // update version string version = MapDBVersions::VERSION_0_2_0.to_string(); } // break at the current version MapDBVersions::VERSION_0_2_0 => { break; } _ => unreachable!("version string {} not known", version), } } Ok(()) } }