From 8b3e8c70ae3b6461dd1a00497f3b428cfdd2ab2f Mon Sep 17 00:00:00 2001 From: projectmoon Date: Mon, 7 Dec 2020 20:32:02 +0000 Subject: [PATCH] Serialize characters as protobufs. Skeletal character creation. Implements the absolute basics of the character creation flow, AKA most things are missing. Integrates a method of storing character data, support for that in the database, and a working character creation page. The only thing the page does at the moment is create a hardcoded basic CofD character sheet and save it to the database. There is no ability to change game system, fill in extra details, etc. There's also no ability to edit anything. Also added basic links to the registration and create new character pages. --- Cargo.lock | 149 ++++++++++++++++++ Cargo.toml | 5 +- build.rs | 3 + .../2020-12-02-213704_characters/up.sql | 4 +- proto/cofd.proto | 79 ++++++++++ src/db.rs | 42 +++-- src/errors.rs | 3 + src/models.rs | 1 + src/models/characters.rs | 85 +++++++--- src/models/proto.rs | 15 ++ src/routes/characters.rs | 43 ++++- src/routes/common.rs | 5 + src/routes/root.rs | 12 +- src/schema.rs | 4 +- templates/index.html.tera | 6 +- templates/login.html.tera | 4 + templates/new_character.html.tera | 13 +- 17 files changed, 430 insertions(+), 43 deletions(-) create mode 100644 build.rs create mode 100644 proto/cofd.proto create mode 100644 src/models/proto.rs 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 %}