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.
This commit is contained in:
jeff 2020-12-07 20:32:02 +00:00
parent ec5be56858
commit d6a80a7996
17 changed files with 430 additions and 43 deletions

149
Cargo.lock generated
View File

@ -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"

View File

@ -3,10 +3,13 @@ name = "tenebrous-sheets"
version = "0.1.0"
authors = ["jeff <jeff@agnos.is>"]
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"

3
build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
prost_build::compile_protos(&["proto/cofd.proto"], &["proto/"]).unwrap();
}

View File

@ -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
);

79
proto/cofd.proto Normal file
View File

@ -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<string, Skill> physical_skills = 16;
map<string, Skill> mental_skills = 17;
map<string, Skill> 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<string, string> other_data = 27;
}
message ChangelingSheet {
CofdSheet base = 1;
}

View File

@ -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<Option<User>>;
fn load_user(&self, for_username: &str) -> QueryResult<Option<User>>;
fn insert_user(&self, new_user: &NewUser) -> QueryResult<User>;
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<CharacterEntry>>;
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>>;
fn load_character(&self, character_id: i32) -> QueryResult<Option<CharacterEntry>>;
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>>;
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<Option<User>> {
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<Vec<CharacterEntry>> {
fn load_character_list(&self, for_user_id: i32) -> QueryResult<Vec<StrippedCharacter>> {
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<Option<CharacterEntry>> {
fn load_character(&self, character_id: i32) -> QueryResult<Option<Character>> {
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)?;

View File

@ -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)]

View File

@ -1,2 +1,3 @@
pub mod characters;
pub mod proto;
pub mod users;

View File

@ -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<Vec<u8>>,
}
/// 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<CharacterEntry> {
let character_is_visible = |c: CharacterEntry| {
if c.viewable || user.map(|u| u.id) == Some(c.user_id) {
Some(c)
fn as_visible_for(self, user: Option<&User>) -> Option<Self>
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<u8>,
}
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],
}

15
src/models/proto.rs Normal file
View File

@ -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
}
}
}

View File

@ -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<rocket::Route> {
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("/<username>/<character_id>")]
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<Templat
Ok(Template::render("new_character", context))
}
#[post("/new", data = "<form>")]
fn create_new_character(
form: Form<NewCharacterForm>,
logged_in_user: &User,
conn: TenebrousDbConn,
) -> Result<Redirect, Error> {
//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::<CofdSheet>(),
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))
}

View File

@ -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))
}

View File

@ -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<rocket::Route> {
/// 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<Template, Error> {
let characters = conn.load_character_list(user.id)?;
let characters: Vec<StrippedCharacter> = 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,

View File

@ -4,7 +4,9 @@ table! {
user_id -> Integer,
viewable -> Bool,
character_name -> Text,
character_data -> Nullable<Binary>,
data_type -> Text,
data_version -> Integer,
data -> Binary,
}
}

View File

@ -8,12 +8,16 @@
{% for char in characters %}
<li>
<a href="characters/{{ user.username }}/{{char.id}}">
{{ char.name }}
{{ char.character_name }}
</a>
</li>
{% endfor %}
</ul>
<div>
<a href="/characters/new">Create New</a>
</div>
<p>
<form action="/logout" method="post">
<input type="submit" value="Logout" />

View File

@ -17,5 +17,9 @@
<input type="password" name="password" id="password" value="" />
<p><input type="submit" value="Login"></p>
</form>
<div>
<a href="/register">Register</a>
</div>
</div>
{% endblock content %}

View File

@ -2,6 +2,17 @@
{% block content %}
<div>
New character page.
New character page.
<form action="/characters/new" method="post">
<div>
<label for="name">Name:</label>
<input id="name" name="name" type="text" />
</div>
<div>
<input type="submit" value="Create Character" />
</div>
</form>
</div>
{% endblock content %}