engine/map/src/map_db.rs
2024-08-23 13:22:09 +02:00

943 lines
26 KiB
Rust

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<f32>,
pub rotation: Rad<f32>,
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<Path>, name: &str, width: u32, height: u32) -> Result<Self> {
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<Path>) -> Result<Self> {
let mut me = Self::open_file(path)?;
me.check_version()?;
me.read_meta_data()?;
Ok(me)
}
fn open_file(path: impl AsRef<Path>) -> Result<Self> {
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<HashMap<u32, u32>> {
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<HashMap<u32, f64>> {
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<Vec<DBTextureInfo>> {
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<Vec<DBEntityInfo>> {
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<Vec<DBEntityInfo>> {
self.read_ent_pos("entitypositions")
}
pub fn read_spawn_locations(&self) -> Result<Vec<DBEntityInfo>> {
self.read_ent_pos("spawnlocations")
}
pub fn read_leave_locations(&self) -> Result<Vec<DBEntityInfo>> {
self.read_ent_pos("leavelocations")
}
pub fn read_mob_spawn_locations(&self) -> Result<Vec<DBNPCSpawnInfo>> {
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<Vec<DBBossSpawnInfo>> {
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<f32>,
rotation: Rad<f32>,
) -> 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<String, rusqlite::Error> {
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(())
}
}