diff --git a/Cargo.lock b/Cargo.lock index dc3b214..f6167ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" + [[package]] name = "arrayref" version = "0.3.6" @@ -203,6 +209,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "cfg-if" version = "0.1.10" @@ -350,6 +362,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "error-chain" version = "0.12.4" @@ -378,6 +396,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + [[package]] name = "fsevent" version = "0.4.0" @@ -466,6 +490,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.17" @@ -585,6 +618,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.6" @@ -742,6 +784,12 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "multimap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1255076139a83bb467426e7f8d0134968a8118844faa755985e077cf31850333" + [[package]] name = "net2" version = "0.2.36" @@ -915,6 +963,16 @@ dependencies = [ "sha-1", ] +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pkg-config" version = "0.3.19" @@ -955,6 +1013,57 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "prost" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce49aefe0a6144a45de32927c77bd2859a5f7677b55f220ae5b744e87389c212" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b10678c913ecbd69350e8535c3aef91a8676c0773fc1d7b95cdd196d7f2f26" +dependencies = [ + "bytes", + "heck", + "itertools", + "log 0.4.11", + "multimap", + "petgraph", + "prost", + "prost-types", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.53", +] + +[[package]] +name = "prost-types" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "quote" version = "0.6.13" @@ -1049,6 +1158,15 @@ version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "rocket" version = "0.4.6" @@ -1299,12 +1417,28 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + [[package]] name = "tenebrous-sheets" version = "0.1.0" dependencies = [ "diesel", "log 0.4.11", + "prost", + "prost-build", "rand", "rocket", "rocket_contrib", @@ -1501,6 +1635,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -1575,6 +1715,15 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/Cargo.toml b/Cargo.toml index e5b30ad..540749d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,13 @@ name = "tenebrous-sheets" version = "0.1.0" authors = ["jeff "] edition = "2018" +build = "build.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[build-dependencies] +prost-build = "0.6" [dependencies] +prost = "0.6" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5834bb4 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + prost_build::compile_protos(&["proto/cofd.proto"], &["proto/"]).unwrap(); +} diff --git a/migrations/2020-12-02-213704_characters/up.sql b/migrations/2020-12-02-213704_characters/up.sql index 2936d77..146e333 100644 --- a/migrations/2020-12-02-213704_characters/up.sql +++ b/migrations/2020-12-02-213704_characters/up.sql @@ -3,5 +3,7 @@ CREATE TABLE characters( user_id INTEGER NOT NULL, viewable BOOLEAN NOT NULL, character_name TEXT NOT NULL, - character_data BLOB NUT NULL + data_type TEXT NOT NULL, + data_version INTEGER NOT NULL, + data BLOB NOT NULL ); diff --git a/proto/cofd.proto b/proto/cofd.proto new file mode 100644 index 0000000..2cdf07d --- /dev/null +++ b/proto/cofd.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package models.proto.cofd; + +//Base sheet for Chronicles of Darkness systems. +message CofdSheet { + message Merit { + int32 dots = 1; + string name = 2; + } + + message Condition { + string name = 1; + } + + ///Entry for a skill + message Skill { + int32 dots = 1; + string name = 2; + sint32 untrained_penalty = 3; + repeated string specializations = 4; + } + + //A generic item with a name, physical description, and rules text. + message Item { + string name = 1; + string description = 2; + string rules = 3; + } + + //An entry for an attack. Usually a weapon. + message Attack { + string name = 1; + int32 dice_pool = 2; + int32 damage = 3; + int32 range = 4; + sint32 initiative_modifier = 5; + int32 size = 6; + } + + string name = 1; + string player = 2; + string campaign = 3; + string description = 4; + + int32 strength = 6; + int32 dexterity = 7; + int32 stamina = 8; + + int32 intelligence = 9; + int32 wits = 10; + int32 resolve = 11; + + int32 presence = 12; + int32 manipulation = 13; + int32 composure = 14; + + map physical_skills = 16; + map mental_skills = 17; + map social_skills = 18; + + repeated Merit merits = 15; + repeated Condition conditions = 19; + + int32 size = 20; + int32 health = 21; + int32 willpower = 22; + int32 experience_points = 23; + int32 beats = 24; + + repeated Item items = 25; + repeated Attack attacks = 26; + + map other_data = 27; +} + +message ChangelingSheet { + CofdSheet base = 1; +} \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index eeb7489..004b5b6 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,5 +1,6 @@ -use crate::models::characters::{CharacterEntry, NewCharacter}; +use crate::models::characters::{Character, NewCharacter, StrippedCharacter}; use crate::models::users::{NewUser, User}; +use crate::schema::characters; use diesel::prelude::*; use diesel::SqliteConnection; @@ -8,21 +9,39 @@ pub(crate) struct TenebrousDbConn(SqliteConnection); pub(crate) trait Dao { fn load_user_by_id(&self, id: i32) -> QueryResult>; + fn load_user(&self, for_username: &str) -> QueryResult>; fn insert_user(&self, new_user: &NewUser) -> QueryResult; - fn load_character_list(&self, for_user_id: i32) -> QueryResult>; + fn load_character_list(&self, for_user_id: i32) -> QueryResult>; - fn load_character(&self, character_id: i32) -> QueryResult>; + fn load_character(&self, character_id: i32) -> QueryResult>; - fn insert_character(&self, new_character: &NewCharacter) -> QueryResult<()>; + fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()>; } +type StrippedCharacterColumns = ( + characters::id, + characters::user_id, + characters::viewable, + characters::character_name, + characters::data_type, + characters::data_version, +); + +const STRIPPED_CHARACTER_COLUMNS: StrippedCharacterColumns = ( + characters::id, + characters::user_id, + characters::viewable, + characters::character_name, + characters::data_type, + characters::data_version, +); + impl Dao for TenebrousDbConn { fn load_user_by_id(&self, user_id: i32) -> QueryResult> { use crate::schema::users::dsl::*; - users.filter(id.eq(user_id)).first(&self.0).optional() } @@ -48,12 +67,15 @@ impl Dao for TenebrousDbConn { .first(&self.0)?) } - fn load_character_list(&self, for_user_id: i32) -> QueryResult> { + fn load_character_list(&self, for_user_id: i32) -> QueryResult> { use crate::schema::characters::dsl::*; - characters.filter(user_id.eq(for_user_id)).load(&self.0) + characters + .filter(user_id.eq(for_user_id)) + .select(STRIPPED_CHARACTER_COLUMNS) + .load(&self.0) } - fn load_character(&self, character_id: i32) -> QueryResult> { + fn load_character(&self, character_id: i32) -> QueryResult> { use crate::schema::characters::dsl::*; characters @@ -62,9 +84,7 @@ impl Dao for TenebrousDbConn { .optional() } - fn insert_character(&self, new_character: &NewCharacter) -> QueryResult<()> { - use crate::schema::characters; - + fn insert_character(&self, new_character: NewCharacter) -> QueryResult<()> { diesel::insert_into(characters::table) .values(new_character) .execute(&self.0)?; diff --git a/src/errors.rs b/src/errors.rs index b062e4e..527ca25 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,9 @@ pub enum Error { #[error("query error: {0}")] QueryError(#[from] diesel::result::Error), + + #[error("serialization error: {0}")] + SerializationError(#[from] prost::EncodeError), } #[derive(Error, Debug)] diff --git a/src/models.rs b/src/models.rs index e646cd7..cb0c1b1 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,2 +1,3 @@ pub mod characters; +pub mod proto; pub mod users; diff --git a/src/models/characters.rs b/src/models/characters.rs index ad7b3cf..315b906 100644 --- a/src/models/characters.rs +++ b/src/models/characters.rs @@ -2,34 +2,73 @@ use crate::models::users::User; use crate::schema::characters; use serde_derive::Serialize; -/// An entry that appears in a user's character list. Properties are -/// in order of table columns. -#[derive(Serialize, Debug, Queryable)] -pub struct CharacterEntry { - pub id: i32, - pub user_id: i32, - pub viewable: bool, - pub name: String, - //TODO don't need to carry around character data for this. - pub data: Option>, -} +/// Control system visibility of a character for a particular user. +/// Implemented as a trait because there are multiple character +/// structs that need this. +pub(crate) trait Visibility { + /// User ID that owns this character. + fn user_id(&self) -> i32; + + /// If the character is publicly visible. + fn viewable(&self) -> bool; -impl CharacterEntry { /// Transform to an Option that holds the character, if the /// character is viewable to a potentially existing user. A /// character is "visible" if the public viewable property is set /// to true, or the user is the owner of the character. Consumes /// self. - pub fn as_visible_for(self, user: Option<&User>) -> Option { - let character_is_visible = |c: CharacterEntry| { - if c.viewable || user.map(|u| u.id) == Some(c.user_id) { - Some(c) - } else { - None - } - }; + fn as_visible_for(self, user: Option<&User>) -> Option + where + Self: std::marker::Sized, + { + if self.viewable() || user.map(|u| u.id) == Some(self.user_id()) { + Some(self) + } else { + None + } + } +} - Some(self).and_then(character_is_visible) +/// An entry that appears in a user's character list. Properties are +/// in order of table columns. +#[derive(Serialize, Debug, Queryable)] +pub struct Character { + pub id: i32, + pub user_id: i32, + pub viewable: bool, + pub character_name: String, + pub data_type: String, + pub data_version: i32, + pub data: Vec, +} + +impl Visibility for Character { + fn user_id(&self) -> i32 { + self.user_id + } + + fn viewable(&self) -> bool { + self.viewable + } +} + +#[derive(Serialize, Debug, Queryable)] +pub struct StrippedCharacter { + pub id: i32, + pub user_id: i32, + pub viewable: bool, + pub character_name: String, + pub data_type: String, + pub data_version: i32, +} + +impl Visibility for StrippedCharacter { + fn user_id(&self) -> i32 { + self.user_id + } + + fn viewable(&self) -> bool { + self.viewable } } @@ -41,5 +80,7 @@ pub struct NewCharacter<'a> { pub user_id: i32, pub viewable: bool, pub character_name: &'a str, - pub character_data: &'a [u8], + pub data_type: &'a str, + pub data_version: i32, + pub data: &'a [u8], } diff --git a/src/models/proto.rs b/src/models/proto.rs new file mode 100644 index 0000000..2b74db4 --- /dev/null +++ b/src/models/proto.rs @@ -0,0 +1,15 @@ +/// Contains the generated Chronicles of Darkness-related protocol +/// buffer types. +pub mod cofd { + include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs")); + + pub(crate) trait DerivedStats { + fn speed(&self) -> i32; + } + + impl DerivedStats for CofdSheet { + fn speed(&self) -> i32 { + self.size + self.stamina + } + } +} diff --git a/src/routes/characters.rs b/src/routes/characters.rs index 527bdd9..944ea65 100644 --- a/src/routes/characters.rs +++ b/src/routes/characters.rs @@ -1,6 +1,8 @@ use crate::db::{Dao, TenebrousDbConn}; use crate::errors::Error; +use crate::models::characters::Visibility; use crate::models::users::User; +use rocket::request::Form; use rocket::response::Redirect; use rocket_contrib::templates::Template; use std::collections::HashMap; @@ -9,11 +11,17 @@ pub(crate) fn routes() -> Vec { routes![ view_character, new_character, + create_new_character, new_character_not_logged_in, edit_character ] } +#[derive(FromForm)] +struct NewCharacterForm { + name: String, //TODO add game system +} + #[get("//")] fn view_character( character_id: i32, @@ -29,7 +37,7 @@ fn view_character( .ok_or(Error::NotFound)?; let mut context = HashMap::new(); - context.insert("name", character.name); + context.insert("name", character.character_name); context.insert("username", user.username); Ok(Template::render("view_character", context)) } @@ -40,6 +48,37 @@ fn new_character(logged_in_user: &User, conn: TenebrousDbConn) -> Result, + logged_in_user: &User, + conn: TenebrousDbConn, +) -> Result { + //TODO redirect to character edit page + //TODO redirect back to new character page with an error and filled-out form if validation errors. + //TODO add game system. + use crate::models::characters::NewCharacter; + use crate::models::proto::cofd::CofdSheet; + use prost::bytes::BytesMut; + use prost::Message; + + let new_character = CofdSheet::default(); + let mut buf = BytesMut::with_capacity(std::mem::size_of_val(&new_character)); + new_character.encode(&mut buf)?; + + let insert = NewCharacter { + user_id: logged_in_user.id, + viewable: true, + character_name: &form.name, + data_type: std::any::type_name::(), + data_version: 1, + data: &buf, + }; + + conn.insert_character(insert)?; + Ok(super::common::redirect_to_index()) +} + #[get("/new", rank = 2)] fn new_character_not_logged_in() -> Redirect { super::common::redirect_to_login() @@ -61,7 +100,7 @@ fn edit_character( } let mut context = HashMap::new(); - context.insert("name", character.name); + context.insert("name", character.character_name); context.insert("username", owner.username); Ok(Template::render("view_character", context)) } diff --git a/src/routes/common.rs b/src/routes/common.rs index 7dd6bcb..752131b 100644 --- a/src/routes/common.rs +++ b/src/routes/common.rs @@ -4,3 +4,8 @@ use rocket::response::Redirect; pub(super) fn redirect_to_login() -> Redirect { Redirect::to(uri!(super::auth::login_page)) } + +/// Common redirect to the index page. +pub(super) fn redirect_to_index() -> Redirect { + Redirect::to(uri!(super::root::index)) +} diff --git a/src/routes/root.rs b/src/routes/root.rs index 2afb8a2..989adbd 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -1,6 +1,7 @@ use crate::db::{Dao, TenebrousDbConn}; use crate::errors::Error; -use crate::models::{characters::CharacterEntry, users::User}; +use crate::models::characters::Visibility; +use crate::models::{characters::StrippedCharacter, users::User}; use rocket::response::Redirect; use rocket_contrib::templates::Template; use serde_derive::Serialize; @@ -12,13 +13,18 @@ pub fn routes() -> Vec { /// Information to display to the user on their home page. #[derive(Serialize)] pub struct UserHomeContext<'a> { - pub characters: &'a [CharacterEntry], + pub characters: &'a [StrippedCharacter], pub user: &'a User, } #[get("/")] fn user_index(user: &User, conn: TenebrousDbConn) -> Result { - let characters = conn.load_character_list(user.id)?; + let characters: Vec = conn + .load_character_list(user.id)? + .into_iter() + .map(|c| c.as_visible_for(Some(user))) + .filter_map(|c| c) + .collect(); let context = UserHomeContext { characters: &characters, diff --git a/src/schema.rs b/src/schema.rs index b819420..bc4efd0 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,7 +4,9 @@ table! { user_id -> Integer, viewable -> Bool, character_name -> Text, - character_data -> Nullable, + data_type -> Text, + data_version -> Integer, + data -> Binary, } } diff --git a/templates/index.html.tera b/templates/index.html.tera index adb082b..7ab16eb 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -8,12 +8,16 @@ {% for char in characters %}
  • - {{ char.name }} + {{ char.character_name }}
  • {% endfor %} + +

    diff --git a/templates/login.html.tera b/templates/login.html.tera index ca2f2fb..90d7fdd 100644 --- a/templates/login.html.tera +++ b/templates/login.html.tera @@ -17,5 +17,9 @@

    + +
    + Register +
    {% endblock content %} diff --git a/templates/new_character.html.tera b/templates/new_character.html.tera index a1eb8b8..9049600 100644 --- a/templates/new_character.html.tera +++ b/templates/new_character.html.tera @@ -2,6 +2,17 @@ {% block content %}
    -New character page. + New character page. + +
    +
    + + +
    + +
    + +
    +
    {% endblock content %}