Start of adding protobuf-based editable character sheet.
This commit is contained in:
parent
b95bad440b
commit
c0a48245b1
5
build.rs
5
build.rs
|
@ -3,6 +3,9 @@ fn main() {
|
|||
config.type_attribute(".", "#[derive(Serialize)]");
|
||||
config.type_attribute(".", "#[serde(rename_all = \"camelCase\")]");
|
||||
config
|
||||
.compile_protos(&["proto/cofd.proto"], &["src/", "proto/"])
|
||||
.compile_protos(
|
||||
&["proto/cofd.proto", "proto/cofd_api.proto"],
|
||||
&["src/", "proto/"],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
syntax = "proto3";
|
||||
import "cofd.proto";
|
||||
|
||||
package models.proto.cofd.api;
|
||||
|
||||
//Update basic information about a Chronicles of Darkness (or
|
||||
//derivative system) character sheet. This is a straight overwrite of
|
||||
//all basic information on the sheet.
|
||||
message BasicInfo {
|
||||
string name = 1;
|
||||
string gender = 2;
|
||||
string concept = 3;
|
||||
string chronicle = 4;
|
||||
int32 age = 5;
|
||||
}
|
||||
|
||||
//Update all attributes in a Chronicles of Darkness character (or
|
||||
//derivative system) character sheet. This is a straight overwrite of
|
||||
//all basic information on the sheet.
|
||||
message Attributes {
|
||||
int32 strength = 1;
|
||||
int32 dexterity = 2;
|
||||
int32 stamina = 3;
|
||||
|
||||
int32 intelligence = 4;
|
||||
int32 wits = 5;
|
||||
int32 resolve = 6;
|
||||
|
||||
int32 presence = 7;
|
||||
int32 manipulation = 8;
|
||||
int32 composure = 9;
|
||||
}
|
||||
|
||||
message Attribute {
|
||||
string name = 1;
|
||||
int32 value = 2;
|
||||
}
|
||||
|
||||
//Update skill entries in a Chronicles of Darkness character sheet.
|
||||
//This is a straight overwrite of all skills in the sheet.
|
||||
message Skills {
|
||||
repeated CofdSheet.Skill physical_skills = 1;
|
||||
repeated CofdSheet.Skill mental_skills = 2;
|
||||
repeated CofdSheet.Skill social_skills = 3;
|
||||
}
|
||||
|
||||
//Add a Condition to a Chronicles of Darkness character sheet.
|
||||
message Condition {
|
||||
string name = 1;
|
||||
}
|
|
@ -25,6 +25,9 @@ pub enum Error {
|
|||
|
||||
#[error("deserialization error: {0}")]
|
||||
DeserializationError(#[from] prost::DecodeError),
|
||||
|
||||
#[error("i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
|
|
@ -31,6 +31,7 @@ async fn main() -> Result<(), rocket::error::Error> {
|
|||
.collect()
|
||||
};
|
||||
|
||||
let api_routes = routes::api::routes();
|
||||
let character_routes = routes::characters::routes();
|
||||
let catchers = catchers::catchers();
|
||||
|
||||
|
@ -39,6 +40,11 @@ async fn main() -> Result<(), rocket::error::Error> {
|
|||
.attach(db::TenebrousDbConn::fairing())
|
||||
.mount("/", root_routes)
|
||||
.mount("/characters", character_routes)
|
||||
.mount("/api", api_routes)
|
||||
.mount(
|
||||
"/scripts",
|
||||
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/static/scripts")),
|
||||
)
|
||||
.mount(
|
||||
"/protos",
|
||||
StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/proto")),
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
use crate::errors::Error;
|
||||
use rocket::data::{Data, FromData, Outcome, ToByteUnit};
|
||||
use rocket::request::Request;
|
||||
use std::default::Default;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Contains the generated Chronicles of Darkness-related protocol
|
||||
/// buffer types.
|
||||
pub mod cofd {
|
||||
include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.rs"));
|
||||
|
||||
pub mod api {
|
||||
include!(concat!(env!("OUT_DIR"), "/models.proto.cofd.api.rs"));
|
||||
}
|
||||
|
||||
// TODO these values are not available in tera templates, so how to
|
||||
// handle?
|
||||
pub(crate) trait DerivedStats {
|
||||
fn speed(&self) -> i32;
|
||||
}
|
||||
|
@ -13,3 +25,46 @@ pub mod cofd {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct wrapping a protobuf that allows it to be used as binary
|
||||
/// data submitted via POST using fetch API. Can automatically be
|
||||
/// dereferenced into its wrapped type.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Proto<T>(T)
|
||||
where
|
||||
T: prost::Message + Default;
|
||||
|
||||
/// Converts the body of a POST request containing encoded protobuf
|
||||
/// data into the wrapped type.
|
||||
#[rocket::async_trait]
|
||||
impl<T> FromData for Proto<T>
|
||||
where
|
||||
T: prost::Message + Default,
|
||||
{
|
||||
type Error = crate::errors::Error;
|
||||
|
||||
async fn from_data(_req: &Request<'_>, data: Data) -> Outcome<Self, Error> {
|
||||
use rocket::http::Status;
|
||||
|
||||
let bytes: Vec<u8> = match data.open(2.mebibytes()).stream_to_vec().await {
|
||||
Ok(read_bytes) => read_bytes,
|
||||
Err(e) => return Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())),
|
||||
};
|
||||
|
||||
match T::decode(bytes.as_ref()) {
|
||||
Ok(decoded) => Outcome::Success(Proto(decoded)),
|
||||
Err(e) => Outcome::Failure((Status::new(422, "invalid protobuf"), e.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Proto<T>
|
||||
where
|
||||
T: prost::Message + Default,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod characters;
|
||||
pub mod common;
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
use crate::db::{Dao, TenebrousDbConn};
|
||||
use crate::errors::Error;
|
||||
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||
use crate::models::proto::{cofd::*, Proto};
|
||||
use crate::models::users::User;
|
||||
use rocket_contrib::templates::Template;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||
routes![
|
||||
cofd::update_basic_info,
|
||||
cofd::update_attributes,
|
||||
cofd::update_attribute,
|
||||
cofd::update_skills,
|
||||
cofd::add_condition,
|
||||
cofd::remove_condition
|
||||
]
|
||||
}
|
||||
|
||||
/// Protobuf-based REST endpoints for editing a character.
|
||||
mod cofd {
|
||||
use super::*;
|
||||
use crate::models::proto::{cofd::api::*, cofd::*, Proto};
|
||||
|
||||
#[post("/cofd/<owner>/<character_id>/basic-info", data = "<info>")]
|
||||
pub(super) fn update_basic_info<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<BasicInfo>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
|
||||
#[post("/cofd/<owner>/<character_id>/attributes", data = "<info>")]
|
||||
pub(super) fn update_attributes<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<Attributes>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
|
||||
#[post("/cofd/<owner>/<character_id>/attribute/<attribute>", data = "<info>")]
|
||||
pub(super) fn update_attribute<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
attribute: String,
|
||||
info: Proto<Attribute>,
|
||||
) -> &'a str {
|
||||
println!("incoming request is {:#?}", info);
|
||||
"lol"
|
||||
}
|
||||
|
||||
#[post("/cofd/<owner>/<character_id>/skills", data = "<info>")]
|
||||
pub(super) fn update_skills<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<Skills>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
|
||||
#[put("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
|
||||
pub(super) fn add_condition<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<Condition>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
|
||||
#[delete("/cofd/<owner>/<character_id>/conditions", data = "<info>")]
|
||||
pub(super) fn remove_condition<'a>(
|
||||
owner: String,
|
||||
character_id: i32,
|
||||
info: Proto<Condition>,
|
||||
) -> &'a str {
|
||||
"lol"
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ use crate::errors::Error;
|
|||
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||
use crate::models::users::User;
|
||||
use rocket_contrib::templates::Template;
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod edit;
|
||||
mod new;
|
||||
|
||||
pub(crate) fn routes() -> Vec<rocket::Route> {
|
||||
|
@ -13,7 +13,7 @@ pub(crate) fn routes() -> Vec<rocket::Route> {
|
|||
new::new_character_page,
|
||||
new::new_character_submit,
|
||||
new::new_character_not_logged_in,
|
||||
edit_character
|
||||
edit::edit_character_page
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -62,28 +62,3 @@ async fn view_character(
|
|||
let template = view_character_template(user, character)?;
|
||||
Ok(template)
|
||||
}
|
||||
|
||||
#[get("/<owner>/<character_id>/edit")]
|
||||
async fn edit_character(
|
||||
character_id: i32,
|
||||
owner: String,
|
||||
logged_in_user: Option<&User>,
|
||||
conn: TenebrousDbConn,
|
||||
) -> Result<Template, Error> {
|
||||
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
||||
|
||||
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
||||
let character = conn
|
||||
.load_character(character_id)
|
||||
.await?
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
if logged_in_user != &owner {
|
||||
return Err(Error::NoPermission);
|
||||
}
|
||||
|
||||
let mut context = HashMap::new();
|
||||
context.insert("name", character.character_name);
|
||||
context.insert("username", owner.username);
|
||||
Ok(Template::render("view_character", context))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
use crate::db::{Dao, TenebrousDbConn};
|
||||
use crate::errors::Error;
|
||||
use crate::models::characters::{Character, CharacterDataType, DynCharacterData, Visibility};
|
||||
use crate::models::users::User;
|
||||
use rocket_contrib::templates::Template;
|
||||
use serde::Serialize;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditCharacterContext<'a> {
|
||||
pub name: &'a str,
|
||||
pub username: &'a str,
|
||||
pub data_type: &'a CharacterDataType,
|
||||
pub sheet: Box<DynCharacterData>,
|
||||
pub state: FormStateContext<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FormStateContext<'a> {
|
||||
pub selected_system: &'a CharacterDataType,
|
||||
}
|
||||
|
||||
#[get("/<owner>/<character_id>/edit")]
|
||||
pub(super) async fn edit_character_page(
|
||||
character_id: i32,
|
||||
owner: String,
|
||||
logged_in_user: Option<&User>,
|
||||
conn: TenebrousDbConn,
|
||||
) -> Result<Template, Error> {
|
||||
let owner = conn.load_user(owner).await?.ok_or(Error::NotFound)?;
|
||||
|
||||
let logged_in_user = logged_in_user.ok_or(Error::NotLoggedIn)?;
|
||||
let character = conn
|
||||
.load_character(character_id)
|
||||
.await?
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
if logged_in_user != &owner {
|
||||
return Err(Error::NoPermission);
|
||||
}
|
||||
|
||||
let context = EditCharacterContext {
|
||||
name: &character.character_name,
|
||||
username: &owner.username,
|
||||
data_type: &character.data_type,
|
||||
sheet: character.dyn_deserialize()?,
|
||||
state: FormStateContext {
|
||||
selected_system: &character.data_type,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Template::render("characters/edit_character", context))
|
||||
}
|
|
@ -7,7 +7,7 @@ use rocket_contrib::templates::Template;
|
|||
use serde_derive::Serialize;
|
||||
|
||||
pub fn routes() -> Vec<rocket::Route> {
|
||||
routes![index, user_index]
|
||||
routes![index, user_index, proto_test]
|
||||
}
|
||||
|
||||
/// Information to display to the user on their home page.
|
||||
|
@ -39,3 +39,12 @@ async fn user_index(user: &User, conn: TenebrousDbConn) -> Result<Template, Erro
|
|||
fn index() -> Redirect {
|
||||
super::common::redirect_to_login()
|
||||
}
|
||||
|
||||
use crate::models::proto::{cofd::*, Proto};
|
||||
|
||||
#[post("/proto-test", data = "<buf>")]
|
||||
async fn proto_test<'a>(buf: Proto<CofdSheet>) -> &'a str {
|
||||
println!("buf is {:#?}", buf);
|
||||
println!("str is: {}", buf.strength);
|
||||
"lol"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
function makeAPI(root) {
|
||||
const Attribute = root.lookupType("models.proto.cofd.api.Attribute");
|
||||
|
||||
const attributeResource = (username, characterID, attribute) =>
|
||||
'/api/cofd/' + username + '/' + characterID + '/attribute/' + attribute;
|
||||
|
||||
async function updateAttribute(params) {
|
||||
const { username, characterID, attribute, newValue } = params;
|
||||
|
||||
let req = Attribute.create({
|
||||
name: attribute,
|
||||
value: parseInt(newValue)
|
||||
});
|
||||
|
||||
const resource = attributeResource(username, characterID, attribute);
|
||||
|
||||
let resp = await fetch(resource, {
|
||||
method: 'POST',
|
||||
body: Attribute.encode(req).finish()
|
||||
}).then(async resp => {
|
||||
console.log("resp is", await resp.text());
|
||||
}).catch(async err => {
|
||||
console.log("err is", err.text());
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
updateAttribute
|
||||
};
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
(async () => {
|
||||
//TODO start refactoring these into a separate script, and make API calls
|
||||
//take all necessary info (e.g. username and character ID, plus other stuff)
|
||||
//as object params.
|
||||
const root = await protobuf.load("/protos/cofd_api.proto");
|
||||
|
||||
const [, , USERNAME, CHARACTER_ID] = window.location.pathname.split('/');
|
||||
|
||||
const api = makeAPI(root);
|
||||
console.log("api is", api);
|
||||
|
||||
function setupAttributes() {
|
||||
const attributeInputs = document.querySelectorAll('#attributes input[type="number"]');
|
||||
|
||||
Array.from(attributeInputs).forEach(input => {
|
||||
input.addEventListener('change', async function(event) {
|
||||
console.log("updating attr");
|
||||
const attribute = event.target.id;
|
||||
const newValue = parseInt(event.target.value);
|
||||
const params = { username: USERNAME, characterID: CHARACTER_ID, attribute, newValue };
|
||||
await api.updateAttribute(params);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupAttributes();
|
||||
})().catch(e => {
|
||||
alert(e);
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
document.addEventListener('DOMContentLoaded', event => {
|
||||
protobuf.load("/protos/cofd.proto").then(function(root) {
|
||||
console.log("root is", root);
|
||||
let CofdSheet = root.lookupType("models.proto.cofd.CofdSheet");
|
||||
let sheet = CofdSheet.fromObject({ name: 'lol', strength: 100 });
|
||||
let buffer = CofdSheet.encode(sheet).finish();
|
||||
|
||||
fetch('/proto-test', {
|
||||
method: 'POST',
|
||||
body: buffer
|
||||
}).then(async resp => {
|
||||
console.log("resp is", await resp.text());
|
||||
}).catch(async err => {
|
||||
console.log("err is", err.text());
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
{% import "characters/edit_character_macros" as macros %}
|
||||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: Liberation Sans, Arial;
|
||||
}
|
||||
|
||||
#attributes {
|
||||
padding: 4px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#attributes .attributes-section {
|
||||
border: 1px solid gray;
|
||||
border-collapse: collapse;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.attribute label {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 10em;
|
||||
text-align: right;
|
||||
vertical-align: text-bottom;
|
||||
padding: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.attribute input {
|
||||
max-width: 4em;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background-color: lightgray;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
|
||||
<script defer type="text/javascript" src="/scripts/api.js"></script>
|
||||
<script defer type="text/javascript" src="/scripts/characters/edit.js"></script>
|
||||
<h1>Core Sheet</h1>
|
||||
<div>
|
||||
<h1>Name: <input type="text" value="{{name}}" /></h1>
|
||||
<p>System: {{data_type}}</p>
|
||||
<div id="attributes">
|
||||
<div class="attributes-section" id="mentalAttributes">
|
||||
{{ macros::attribute(name="Intelligence", value=sheet.intelligence) }}
|
||||
{{ macros::attribute(name="Wits", value=sheet.wits) }}
|
||||
{{ macros::attribute(name="Resolve", value=sheet.resolve) }}
|
||||
</div>
|
||||
|
||||
<div class="attributes-section" id="physicalAttributes">
|
||||
{{ macros::attribute(name="Strength", value=sheet.strength) }}
|
||||
{{ macros::attribute(name="Dexterity", value=sheet.dexterity) }}
|
||||
{{ macros::attribute(name="Stamina", value=sheet.stamina) }}
|
||||
</div>
|
||||
|
||||
<div class="attributes-section" id="socicalAttributes">
|
||||
{{ macros::attribute(name="Presence", value=sheet.presence) }}
|
||||
{{ macros::attribute(name="Manipulation", value=sheet.manipulation) }}
|
||||
{{ macros::attribute(name="Composure", value=sheet.composure) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -0,0 +1,6 @@
|
|||
{% macro attribute(name, value) %}
|
||||
<div class="attribute">
|
||||
<label for="{{name}}">{{name}}:</label>
|
||||
<input id="{{name}}" name="{{name}}" type="number" min="0" value="{{value}}" />
|
||||
</div>
|
||||
{% endmacro input %}
|
|
@ -1,6 +1,8 @@
|
|||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/protobufjs@6.10.2/dist/protobuf.min.js"></script>
|
||||
<script type="text/javascript" src="/scripts/characters/new-character.js"></script>
|
||||
<div>
|
||||
New character page.
|
||||
|
||||
|
|
Loading…
Reference in New Issue